14 KiB
LIS
+LCS
专题
基础题
O(N^2)
算法
状态表示
f[i]
:以a[i]
结尾的最长上升子序列长度
状态转移
for (int i = 1; i <= n; i++) {
f[i] = 1; //只有a[i]一个数
for (int j = 1; j < i; j++)
if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
}
答案
for (int i = 1; i <= n; i++) res = max(res, f[i]);
数据量增大:N<=100000
,O(LogN*N)
算法
状态表示
f[i]
表示长度为i
的递增子序列中,末尾元素最小的是f[i]
答案
fl
状态转移
① f[]
数组是一个单独上升的数组,这是可以二分的基础
② 每个长度都争取替换掉前面记录数组中第一个大于等于自己的数字,以保证长度不变的情况下,数字最小,因为只有最小才能让后面的其它数字更容易接上去,机会才能更多。
// 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];
}
也可以这样写
memset(f,-0x3f,sizeof f);
// 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 + 1, f + 1 + fl, a[i]) = a[i];
}
状态表示
定义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]
- 如果
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]);
}
答案
f[n][m]
进阶题
朴素O(N^2)
版本
int mx = 0;
//从左到右,求一遍最长上升子序列
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);
mx = max(mx, 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);
mx = max(mx, f[i]);
}
优化O(LogN*N)
版本
// 正着求
f[0] = a[0];
for (int i = 1; i < n; i++) {
if (a[i] > f[fl])
f[++fl] = a[i];
else
*lower_bound(f, f + fl, a[i]) = a[i];
}
// 反着求
g[0] = a[n-1];
for (int i = n - 1; i >= 0; i--) {
if (a[i] > g[gl])
g[++gl] = a[i];
else
*lower_bound(g, g + gl, a[i]) = a[i];
}
// pk的最大结果
res = max(fl, gl);
// 输出
printf("%d\n", res + 1);
//正向求解 LIS问题
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);
}
//反向求解 LIS问题
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);
}
int res = 0;
//因为最终的那个中间点,左边计算了一次,右边双计算了一次,需要减1去重复
for (int i = 1; i <= n; i++) res = max(res, f[i] + g[i] - 1);
//正向求解 LIS问题
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);
}
//反向求解 LIS问题
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);
}
int res = 0;
//因为最终的那个中间点,左边计算了一次,右边双计算了一次,需要减1去重复
for (int i = 1; i <= n; i++) res = max(res, f[i] + g[i] - 1);
//输出
printf("%d\n", n-res);
//正向
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);
AcWing
1012
. 友好城市
① 数对
② 左端点排序
③ 对于右端点组成的数组,求最长上升子序列长度
for (int i = 1; i <= n; i++)cin >> q[i].first >> q[i].second;
sort(q + 1, q + 1 + n);//按first排序
int res = 0;
for (int i = 1; i <= n; i++) {
f[i] = 1;
for (int j = 1; j < i; j++)
if (q[i].second > q[j].second)
f[i] = max(f[i], f[j] + 1);
res = max(res, f[i]);
}
printf("%d\n", res);
typedef pair<int, int> PII;
#define x first
#define y second
PII a[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);
#include <bits/stdc++.h>
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;
}
魔改 最长上升子序列和
数组说明
q[],ql
:已经创建了ql
套导弹系统,q[i]
中记录的是第i
套导弹系统的最大拦截高度
算法思路
- 在已有的所有导弹拦截系统中找到大于当前高度,并且最小的那个,把当前导弹用这套拦截系统进行拦截
- 如果找不到这样的拦截系统,说明当前所有拦截系统的最小拦截高度都小于当前导弹,都无法利用,必须新开一个拦截系统。
- 只有前面所有拦截系统的最后一个拦截高度,都小于当前高度时,才会创建新的,所以,后面的拦截系统,它的尾巴值应该是最大的
举栗子:
q[0]:5
q[1]:6
现在来了一个高度=4
,根据上面的原则,应该放到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
了:
// 第一问
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);
导弹拦截系统的拦截高度即可以 严格单调上升,又可以严格单调下降,此时,用LIS模型是无法解决问题的,考虑到n<50
,可以用dfs+
剪枝
#include <bits/stdc++.h>
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的全干掉了,能到这里的,都是<res的,都可以用来更新答案
return;
}
// 放入上升序列
int k = 0;
// 如果把当前导弹使用上升序列的拦截系统进行拦截,那个选择哪个系统呢?
for (k = 0; k < ul; k++)
if (up[k] <= a[u]) break; // up[0],up[1],up[2],... 这样套拦截系统,其数值来讲,是递减的,因为是因为不能再拦截高度更低的才创建了一套新的拦截系统
// 找出放到哪个拦截系统上去,结果为k
int t = up[k]; // 尝试在第k套系统进行拦截第u个导弹,那么,第u个导弹的高度就是第k套系统的新高度
up[k] = a[u]; // 问题是,我们也不一定非得让第u个导弹使用上升序列拦截系统,也可能使用下降序列拦截系统,所以需要记录当前值,回溯后,尝试下降序列拦截
if (k < ul) // 如果当前这些套拦截系统中可以找到某一套进行拦截
dfs(u + 1, ul, dl); // 接到了某个队伍后面去了,修改队伍的最后最大值即可,队伍数量没有长大
else // 如果当前这些套拦截系统中无法找到某一套进行拦截
dfs(u + 1, ul + 1, dl); // 没有找到任何一个符合最后一个高度小于a[u]的队伍,只能多开一队,up数组长度长大1
up[k] = t; // 回溯
// ----------------------------------------------------------------------
// 放入下降序列
k = 0;
for (k = 0; k < dl; k++)
if (down[k] >= 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;
}
#include <bits/stdc++.h>
using namespace std;
const int N = 3010;
int a[N], b[N];
int f[N][N];
int res;
// O(n^2)
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;
for (int j = 1; j <= n; j++) {
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;
}