## $LIS$+$LCS$专题 #### 一、基础题 **[$AcWing$ $895$. 最长上升子序列](https://www.acwing.com/problem/content/897/)** **时间复杂度** $O(N^2)$ **状态表示** $f[i]$:以$a[i]$结尾的最长上升子序列长度 **状态转移** $f[i] = max(f[i], f[j] + 1)(a[i] > a[j])$ **初始值** $f[i] = 1$ > **解释**:一个以$i$结尾的序列,最少可以是$i$自己,长度是$1$ **代码** ```cpp {.line-numbers} #include using namespace std; const int N = 1010; int n; int a[N], f[N]; int res; int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) { f[i] = 1; // 一个以i结尾的序列,最少可以是i自己,长度是1 for (int j = 1; j < i; j++) if (a[i] > a[j]) // i可以接在j后面 f[i] = max(f[i], f[j] + 1); // 借力于j res = max(res, f[i]); } printf("%d", res); return 0; } ``` **[$AcWing$ $896$. 最长上升子序列 II ](https://www.acwing.com/problem/content/898/)** 数据量增大:$N<=100000$ **思想** 对于同样长度的子串,希望它的末端越小越好,这样后面更易扩展它,使数列更长。 **时间复杂度** $O(LogN*N)$ **状态表示** $f[i]$表示 **长度为$i$** 的递增子序列中,**末尾元素最小** 的是$f[i]$ **答案** $fl$ **特点与总结** ① $f[]$数组是一个单调上升的数组,这是二分的基础 ② 每个长度都争取替换掉前面记录数组中第一个大于等于自己的数字,以保证长度不变的情况下,数字最小。 **学习方法** 举栗子 **代码** ```cpp {.line-numbers} #include using namespace std; const int N = 100010; int n, a[N]; // 数组模拟 int f[N], fl; int main() { cin >> n; for (int i = 0; i < n; i++) cin >> a[i]; // 1、首个元素 f[0] = a[0]; // 2、后续元素开始计算 for (int i = 1; i < n; i++) { if (a[i] > f[fl]) f[++fl] = a[i]; else // 利用STL的二分,在f中查找第一个大于等于a[i]的值,并完成替换 *lower_bound(f, f + fl, a[i]) = a[i]; } // 栈内元素数量就是答案 printf("%d\n", fl + 1); return 0; } ``` **[$AcWing$ $897$. 最长公共子序列](https://www.acwing.com/problem/content/899/)** **状态表示** $f[i][j]$:$a[]$以$i$结尾,$b[]$以$j$结尾的 **最长公共子序列长度** > **说明**:没有说$a[i]$或者$b[j]$一定要出现在最长公共子序列当中!这个最长公共子序列,可能是$a[]$和$b[]$的一些前序组成的,$a[i],b[j]$也可能没有对结果产生贡献。 * 当$a[i]==b[j]$时,看一下两个字符串的前序,发现在少了$a[i],b[j]$后,转化为子问题$f[i-1][j-1]$,问题转化为$$f[i][j]=f[i-1][j-1]+1$$ * 当$a[i] \neq b[j]$时: * 如果$a[i]$不产生贡献,那么把它干掉$f[i-1][j]$ * 如果$b[j]$不产生贡献,那么把它干掉$f[i][j-1]$3 **答案** $f[n][m]$ **代码** ```cpp {.line-numbers} #include using namespace std; const int N = 1010; int n, m; char a[N], b[N]; int f[N][N]; int main() { // 递推式出现f[i-1][j-1],如果i,j从0开始会出现负值,所以下标从1开始 cin >> n >> m >> a + 1 >> b + 1; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) { if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1; else f[i][j] = max(f[i - 1][j], f[i][j - 1]); } printf("%d", f[n][m]); return 0; } ``` #### 二、进阶题 **[$AcWing$ $1017$. 怪盗基德的滑翔翼](https://www.acwing.com/problem/content/1019/)** 朴素$O(N^2)$版本 ```cpp {.line-numbers} #include using namespace std; const int N = 110; int n; // 楼房的个数 int w[N]; // 楼房的高度数组 int f[N]; // LIS结果数组,DP结果 int main() { int T; cin >> T; while (T--) { // 保持好习惯,多组测试数据,记得每次清空结果数组 memset(f, 0, sizeof f); int res = 0; cin >> n; for (int i = 1; i <= n; i++) cin >> w[i]; // 从左到右,求一遍最长上升子序列[朴素O(N^2)版本] for (int i = 1; i <= n; i++) { f[i] = 1; for (int j = 1; j < i; j++) if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1); res = max(res, f[i]); } // 反向求解 LIS问题 for (int i = n; i >= 1; i--) { f[i] = 1; for (int j = n; j > i; j--) if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1); res = max(res, f[i]); } printf("%d\n", res); } return 0; } ``` 优化$O(LogN*N)$版本 ```cpp {.line-numbers} #include using namespace std; const int N = 110; int n; // 楼房的个数 int a[N]; // 楼房的高度数组 // 数组模拟栈 int f[N], fl; int g[N], gl; int res; int main() { int T; cin >> T; while (T--) { // 保持好习惯,多组测试数据,记得每次清空结果数组 memset(f, 0, sizeof f); memset(g, 0, sizeof g); fl = 0, gl = 0; cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; // 正着求 for (int i = 1; i <= n; i++) { if (a[i] > f[fl]) f[++fl] = a[i]; else *lower_bound(f + 1, f + 1 + fl, a[i]) = a[i]; } // 反着求 for (int i = n; i; i--) { if (a[i] > g[gl]) g[++gl] = a[i]; else *lower_bound(g + 1, g + 1 + gl, a[i]) = a[i]; } // pk的最大结果 res = max(fl, gl); // 输出 printf("%d\n", res); } return 0; } ``` **[$AcWing$ $1014$. 登山](https://www.acwing.com/problem/content/1016/)** ```cpp {.line-numbers} #include using namespace std; const int N = 1010; int n; // 山的个数 int a[N]; // 山的高度数组 int f[N]; // 最长上升子序列 int g[N]; // 最长下降子序列 int res; int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; // 正向 for (int i = 1; i <= n; i++) { f[i] = 1; for (int j = 1; j < i; j++) if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1); } // 反向 for (int i = n; i >= 1; i--) { g[i] = 1; for (int j = n; j > i; j--) if (a[i] > a[j]) g[i] = max(g[i], g[j] + 1); } // 每个点,都可能是两者相加的最大位置处,所以,需要枚举每个点,每个点都有资格参评最优点 // 因为最终的那个中间点,左边计算了一次,右边又计算了一次,需要减1去重复 for (int i = 1; i <= n; i++) res = max(res, f[i] + g[i] - 1); // 输出 printf("%d\n", res); return 0; } ``` **[$AcWing$ $482$. 合唱队形](https://www.acwing.com/problem/content/484/)** ```cpp {.line-numbers} #include using namespace std; const int N = 1010; int n, a[N]; int f[N], fl, p1[N]; int g[N], gl, p2[N]; int res; int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; // 正向 f[++fl] = a[1]; p1[1] = 1; for (int i = 2; i <= n; i++) if (a[i] > f[fl]) { f[++fl] = a[i]; p1[i] = fl; } else { int t = lower_bound(f + 1, f + fl + 1, a[i]) - f; f[t] = a[i]; p1[i] = t; } // 反向 g[++gl] = a[n]; p2[n] = 1; for (int i = n - 1; i >= 1; i--) if (a[i] > g[gl]) { g[++gl] = a[i]; p2[i] = gl; } else { int t = lower_bound(g + 1, g + gl + 1, a[i]) - g; g[t] = a[i]; p2[i] = t; } for (int i = 1; i <= n; i++) res = max(res, p2[i] + p1[i] - 1); printf("%d\n", n - res); return 0; } ``` **[$AcWing$ $1012$. 友好城市](https://www.acwing.com/problem/content/1014/)** ① 数对 ② 左端点排序 ③ 对于右端点组成的数组,求最长上升子序列长度 ```cpp {.line-numbers} #include using namespace std; typedef pair PII; #define x first #define y second const int N = 5010; PII a[N]; // 南岸和北岸的一对友好城市的坐标 int f[N]; // 记录以f[i]为结尾的一对友好城市时的,不产生交叉的最多城市对数 int n; // n组友好城市 int res; int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y; sort(a + 1, a + 1 + n); // PII默认按第一维度first进行排序 // LIS for (int i = 1; i <= n; i++) { f[i] = 1; for (int j = 1; j < i; j++) if (a[i].y > a[j].y) f[i] = max(f[i], f[j] + 1); res = max(res, f[i]); } printf("%d\n", res); return 0; } ``` ```cpp {.line-numbers} #include using namespace std; typedef pair PII; #define x first #define y second const int N = 5010; PII a[N]; // 南岸和北岸的一对友好城市的坐标 int f[N], fl; // 数组模拟栈 int n; // n组友好城市 int res; int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y; sort(a + 1, a + 1 + n); f[++fl] = a[1].y; for (int i = 2; i <= n; i++) { if (a[i].y > f[fl]) f[++fl] = a[i].y; else *lower_bound(f + 1, f + 1 + fl, a[i].y) = a[i].y; } printf("%d\n", fl); return 0; } ``` **[$AcWing$ $1016$. 最大上升子序列和](https://www.acwing.com/problem/content/1012/)** ```cpp {.line-numbers} #include using namespace std; // 最大上升子序列和 int n; const int N = 100010; int a[N]; int f[N];//以第i个元素结尾的最大子序列和 int res; int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) { f[i] = a[i]; // 最大上升子序列(个数)这里是1,此处是a[i] for (int j = 1; j < i; j++) // 最大上升子序列(个数)这里是加1,此处是+a[i] if (a[i] > a[j]) f[i] = max(f[i], f[j] + a[i]); res = max(res, f[i]); } printf("%d ", res); return 0; } ``` **魔改 最长上升子序列和** **[$AcWing$ $1010$. 拦截导弹](https://www.acwing.com/problem/content/1012/)** > **题目要求**:但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。 **数组含义** $q[],ql$:已经创建了$ql$套导弹系统,$q[i]$中记录的是第$i$套导弹系统的最大拦截高度 **举栗子,总结规律** $q[0]:5$ $q[1]:6$ > **举的这个栗子对吗?** > 我们思考一下:原来有一套导弹拦截系统,上次的高度是$5$,现在第二颗导弹的高度是$6$,根据题目要求,它不能高于前一发的高度,所以只能再开一个导弹拦截系统,所以,第二个是$6$是正确的。 现在来了一个高度=$4$,根据上面的原则,可以接在$5$后面,也可以接在$6$后面,我们为了不浪费$6$,就选择了接在$5$后面,放到$q[0]$里,修改$q[0]=4$ $q[0]:4$ $q[1]:6$ 原来的数组是单调上升的,修改后值也是在上下夹着中间的,也依然是单调上升的。 再比如: $q[0]:4$ $q[1]:6$ $q[2]:7$ 现在来了一个$a[i]=5$,我们肯定要修改$q[1]=5$,变为: $q[0]:4$ $q[1]:5$ $q[2]:7$ 原来的数组是 **单调上升** 的,修改后也依然是 **单调上升** 的。 既然有这个规律,那么就可以使用二分快速查找大于等于$a[i]$的位置$p$了: ```cpp {.line-numbers} #include using namespace std; const int N = 1010; int a[N], f[N]; // 每个导弹的高度,f[i]:以a[i]结尾的最长不升子序列长度 int q[N], ql; // 需要ql套拦截系统,每个拦截系统最后一个拦截高度为q[i] int n, res; int main() { while (cin >> a[n]) n++; // 第一问 for (int i = 0; i < n; i++) { f[i] = 1; for (int j = 0; j < i; j++) if (a[i] <= a[j]) // 如果前面的比当前的大,说明是不升序列 f[i] = max(f[i], f[j] + 1); res = max(res, f[i]); } // 第二问 for (int i = 0; i < n; i++) { int p = lower_bound(q, q + ql, a[i]) - q; if (p == ql) // 如果没有找到可以接在后面的导弹拦截系统,那么需要创建一套新的拦截系统 q[ql++] = a[i]; else q[p] = a[i]; // 否则就直接接到找到的第k套拦截系统的后面,那么,第k套拦截系统的最后一个拦截高度=q[k]=h[i] } // 输出最长不升序列长度,即:最多能拦截的导弹数 printf("%d\n%d\n", res, ql); return 0; } ``` **[$AcWing$ $187$. 导弹防御系统](https://www.acwing.com/problem/content/189/)** 导弹拦截系统的拦截高度即可以 严格单调上升,又可以严格单调下降,此时,用$LIS$模型是无法解决问题的,考虑到$n<50$,可以用$dfs+$剪枝 ```cpp {.line-numbers} #include using namespace std; const int N = 55; const int INF = 0x3f3f3f3f; int n; int a[N]; int up[N], down[N]; int res; // 本题关键字:贪心+爆搜 /* 功能:暴搜所有可能,配合剪枝,找出最少的拦截系统数量 u: 第几个导弹 ul: 上升拦截系统的数量,配合up 数组使用 dl: 下降拦截系统的数量,配合down数组使用 */ void dfs(int u, int ul, int dl) { if (ul + dl >= res) return; // 伟大的剪枝,不剪枝会TLE~,中途发现已经大于等于res的情况,就返回 if (u == n) { // 走完全程,收集答案 res = min(res, ul + dl); // 因为上面的剪枝,把ul+dl>=res的全干掉了,能到这里的,都是= a[u]) break; t = down[k]; // 保留现场 down[k] = a[u]; if (k < dl) dfs(u + 1, ul, dl); else dfs(u + 1, ul, dl + 1); down[k] = t; // 回溯 } int main() { while (cin >> n, n) { // 多套数据,输入n=0时停止 for (int i = 0; i < n; i++) cin >> a[i]; res = INF; // 防御系统的最少数量 dfs(0, 0, 0); // 开始深搜,更新res的值 printf("%d\n", res); } return 0; } ``` **[$AcWing$ $272$. 最长公共上升子序列](https://www.acwing.com/problem/content/274/)** ```cpp {.line-numbers} #include using namespace std; const int N = 3010; int a[N], b[N]; int f[N][N]; int res; // 通过了 10/13个数据 // O(n^3) int main() { int n; 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++) { // ① 二维DP打表的一般套路,都是可以直接从上一行继承的 // ② 从题意出发,就是a中前i个数字,b中前j个数字,且以b[j]结尾的子序列中长度最大的 // 那么,a中多整出一个数字来,最起码也是f[i-1][j]的值,不能更小 f[i][j] = f[i - 1][j]; // ③ 如果恰好 a[i]==b[j],那么就可以发生转移 if (a[i] == b[j]) { int mx = 1; // 最起码a[i]==b[j],有一个数字是一样嘀~ // f[i-1]是肯定的了,问题是b的前驱在哪里?需要枚举1~j-1 for (int k = 1; k < j; k++) if (a[i] > b[k]) // j可以接在k后面,那么可能的最大值为f[i-1][k]+1 mx = max(mx, f[i - 1][k] + 1); // 更新答案 f[i][j] = max(f[i][j], mx); } } int res = 0; // a数组肯定是火力全开到n就行,b数组中的位置就需要枚举了 for (int i = 1; i <= n; i++) res = max(res, f[n][i]); printf("%d\n", res); return 0; } ``` 前面之所以把$b[j]$换成$a[i]$,是因为方便现在考虑。 由于在枚举计算$f[i][j]$时,却用到的是和内层循环(即$b[j]$)无关的,只和外层循环(即$a[i]$)有关,而我们在计算$f[i][j]$,每次都要用到前$j−1$个数中小于$a[i]$的$f[i-1][k]$最大值,所以我们可以用一个变量$mx$不断更新它来保存前$j−1$的答案,就可以避免重复计算,从而降低时间复杂度。 $O(N^2)$ ```cpp {.line-numbers} #include using namespace std; const int N = 3010; int a[N], b[N]; int f[N][N]; int res; // O(n^2) // f[i][j]—集合:考虑 a 中前 i 个数字,b 中前 j 个数字 ,且当前以 b[j] 结尾的子序列的方案 int main() { int n; 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 mx = 1; // 如果a[i]==b[j],那么LICS最小是1.如果下面的循环中没有一个if被执行,则mx没使上 for (int j = 1; j <= n; j++) { f[i][j] = f[i - 1][j]; // 先继承过来,现实含义:即使a[i]!=b[j],那么最长长度不会因为i的增加而变化,即f[i][j]=f[i-1][j] if (a[i] == b[j]) f[i][j] = mx; if (a[i] > b[j]) mx = max(mx, f[i - 1][j] + 1); } } for (int i = 1; i <= n; i++) res = max(res, f[n][i]); printf("%d\n", res); return 0; } ``` [最长上升子序列 ($LIS$) 详解+例题模板 (全)](https://blog.csdn.net/lxt_Lucia/article/details/81206439)