AcWing算法提高课:动态规划2-最长上升子序列

摘要

895. 最长上升子序列 - AcWing题库:LIS模板题(DP)(O(N^2))

896. 最长上升子序列 II - AcWing题库:LIS模板题(贪心 + 二分)O(N*LogN)

1017. 怪盗基德的滑翔翼 - AcWing题库:LIS二次遍历,求max(上升,下降)

1014. 登山 - AcWing题库:求 先上升再下降 的最大长度

482. 合唱队形 - AcWing题库: 1014的变形,求最少删去多少个数,可以构成先上升再下降的序列

1012. 友好城市 - AcWing题库:两个序列的LIS,一个作为下标,一个作为数值

1016. 最大上升子序列和 - AcWing题库:求最大和,只需要将 dp[j] + 1 改为 dp[j] + a[i](注意最大和长度可能为1)

1010. 拦截导弹 - AcWing题库:覆盖整个序列的最少的不上升子序列的个数 == 最长上升子序列的长度

187. 导弹防御系统 - AcWing题库:覆盖整个序列的最少的子序列个数(上升 或 下降)的个数 -> DFS

272. 最长公共上升子序列 - AcWing题库:LCIS模板题,O(N^2)解法,前缀最大值优化

题谱

895 最长上升子序列

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000 −109≤数列中的数≤109

输入样例:

1
2
7
3 1 2 1 8 5 6

输出样例

1
4

题解

动态规划O(N^2)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4;

int n,res;
int a[maxn],dp[maxn];

int main(void){
cin>>n;
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
}
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(a[i] > a[j]){
dp[i] = max(dp[i], dp[j] + 1);
res = max(res, dp[i]);
}
}
}
cout<<res+1;
return 0;
}

896 最长上升子序列 II

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤100000 −10^9≤数列中的数≤10^9

输入样例

1
2
7
3 1 2 1 8 5 6

输出样例

1
4

题解

二分+贪心O(N*logN)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4;

int n,cnt;
int low[maxn],a[maxn];

int binarySearch(int l, int r, int x){
while(l+1 != r){
int mid = (l+r)/2;
if(low[mid] < x){
l = mid;
}else{
r = mid;
}
}
return r;
}

int main(void){
cin>>n;
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
}
low[++cnt] = a[1];
for(int i = 2 ; i <= n ; i ++){
if(a[i] > low[cnt]){
low[++cnt] = a[i];
}else{
//int j = lower_bound(low+1,low+cnt+1,a[i]) - low;
int j = binarySearch(0, cnt, a[i]);
low[j] = a[i];
}
}
cout<<cnt;
return 0;
}

1017 怪盗基德的滑翔翼

怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。

而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。

有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。

不得已,怪盗基德只能操作受损的滑翔翼逃脱。

假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。

初始时,怪盗基德可以在任何一幢建筑的顶端。

他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。

因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。

他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。

请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

输入格式

输入数据第一行是一个整数K,代表有K组测试数据。

每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。

输出格式

对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。

数据范围

1≤K≤100 1≤N≤100 0<h<10000

输入样例

1
2
3
4
5
6
7
3
8
300 207 155 299 298 170 158 65
8
65 158 170 298 299 155 207 300
10
2 1 3 4 5 6 7 8 9 10

输出样例

1
2
3
6
6
9

题解

怪盗基德可以从左到右滑行,也可以从右到左滑行。基德每次滑行必须严格下降,求最大长度。

相当于计算 Max( 最长上升子序列长度,最长下降子序列长度)。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<bits/stdc++.h>
using namespace std;
const int maxn = 200;

int t,n,res;
int a[maxn],dp[maxn];

void solve(){
res = 0;
cin>>n;
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
}
memset(dp,0,sizeof(dp));
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(a[i] > a[j]){
dp[i] = max(dp[i], dp[j] + 1);
res = max(res, dp[i]);
}
}
}
memset(dp,0,sizeof(dp));
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(a[i] < a[j]){
dp[i] = max(dp[i], dp[j] + 1);
res = max(res, dp[i]);
}
}
}
cout<<res+1<<"\n";
}

int main(void){
cin>>t;
while(t--){
solve();
}
return 0;
}

1014 登山

五一到了,ACM队组织大家去登山观光,队员们发现山上一共有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。

同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。

队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

输入格式

第一行包含整数N,表示景点数量。

第二行包含N个整数,表示每个景点的海拔。

输出格式

输出一个整数,表示最多能浏览的景点数。

数据范围

2≤N≤1000

输入样例

1
2
8
186 186 150 200 160 130 197 220

输出样例

1
4

题解

登山队先上山后下山,且海拔必须严格上升和下降。

本质是 求 先上升后下降 的最大序列长度。因此,我们从左到右遍历一次上升序列,在从右到左遍历一次下降序列。

dp1[i] + 1 表示从1到i的最大长度,dp2[i] + 1表示从n到i的最大长度。

res = max(res, dp1[i] + dp2[i] + 1)

此处不是+2,而是+1,是因为要减去重合点 i。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2000;

int n,res;
int a[maxn],dp1[maxn],dp2[maxn];

int main(void){
cin>>n;
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
}
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(a[i] > a[j]){
dp1[i] = max(dp1[i],dp1[j]+1);
}
}
}
for(int i = n-1 ; i >= 1 ; i --){
for(int j = n ; j >= i+1 ; j --){
if(a[i] > a[j]){
dp2[i] = max(dp2[i], dp2[j]+1);
}
}
}
for(int i = 1 ; i <= n ; i ++){
res = max(res,dp1[i]+dp2[i]);
}
cout<<res+1; // res + 1 + 1 - 1
return 0;
}

482 合唱队形

NN 位同学站成一排,音乐老师要请其中的 (N−K)(N−K) 位同学出列,使得剩下的 KK 位同学排成合唱队形。     

合唱队形是指这样的一种队形:设 KK 位同学从左到右依次编号为 1,2…,K他们的身高分别为 T1,T2,…,TK 则他们的身高满足 T1<…Ti+1>…>TK(1≤i≤K)   

你的任务是,已知所有 N 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

输入格式

输入的第一行是一个整数 N,表示同学的总数。

第二行有 N 个整数,用空格分隔,第 i个整数 Ti 是第 i 位同学的身高(厘米)。

输出格式

输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。

数据范围

2≤N≤100 130≤Ti≤230

输入样例

1
2
8
186 186 150 200 160 130 197 220

输出样例

1
4

题解

这题和1014类似,问最少需要多少人出队,其实就是在问最多剩下多少人。

这个问题和1014相同,使用总数减去结果即可。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<bits/stdc++.h>
using namespace std;
const int maxn = 200;

int n,res;
int a[maxn],dp1[maxn],dp2[maxn];

int main(void){
cin>>n;
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
}
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(a[i] > a[j]){
dp1[i] = max(dp1[i],dp1[j]+1);
}
}
}
for(int i = n-1 ; i >= 1 ; i --){
for(int j = n ; j >= i+1 ; j --){
if(a[i] > a[j]){
dp2[i] = max(dp2[i], dp2[j]+1);
}
}
}
for(int i = 1 ; i <= n ; i ++){
res = max(res,dp1[i]+dp2[i]);
}
cout<<n-(res+1);
return 0;
}

1012 友好城市

Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。

北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。

每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。

编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

输入格式

第1行,一个整数N,表示城市数。

第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。

输出格式

仅一行,输出一个整数,表示政府所能批准的最多申请数。

数据范围

1≤N≤500 0≤xi≤10000

输入样例

1
2
3
4
5
6
7
8
7
22 4
2 6
10 3
15 12
9 8
17 17
4 2

输出样例

1
4

题解

在河的南北两岸分别有n队友好城市,每对友好城市可以修建桥梁。现在给定每对城市的坐标,求最大桥梁数量。这里只需要用北岸城市坐标排序,并将南岸城市的坐标当作数值,求最长上升子序列即可。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4;

int n,res;
int dp[maxn];

struct city{
int a,b;
bool operator<(const city & rhs){
return a < rhs.a;
}
}c[maxn];

int main(void){
cin>>n;
for(int i = 1 ; i <= n ; i ++){
cin>>c[i].a>>c[i].b;
}
sort(c+1,c+n+1);
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(c[i].b > c[j].b){
dp[i] = max(dp[i],dp[j]+1);
res = max(dp[i],res);
}
}
}
cout<<res+1;
return 0;
}

1016 最大上升子序列和

一个数的序列 bi,当 b1<b2<…<bS 的时候,我们称这个序列是上升的。

对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1≤i1<i2<…<iK≤N

比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。

这些子序列中和最大为18,为子序列(1,3,5,9)的和。

你的任务,就是对于给定的序列,求出最大上升子序列和。

注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。

输入格式

输入的第一行是序列的长度N。

第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

输出格式

输出一个整数,表示最大上升子序列和。

数据范围

1≤N≤1000

输入样例

1
2
7
1 7 3 5 9 4 8

输出样例

1
18

题解

求最大长度:dp[i] = max(dp[i], dp[j] + 1);

求最大和:dp[i] = max(dp[i], dp[j] + a[i]);

  • 注意需要考虑 长度为1的最大值,例如:100 1 2 3

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2000;

int n,res;
int a[maxn],dp[maxn];

int main(void){
cin>>n;
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
dp[i] = a[i];
res = max(dp[i], res);
}
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(a[i] > a[j]){
dp[i] = max(dp[i], dp[j] + a[i]);
res = max(dp[i], res);
}
}
}
cout<<res;
return 0;
}

1010 拦截导弹

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

共一行,输入导弹依次飞来的高度。

输出格式

第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

数据范围

雷达给出的高度数据是不大于 30000 的正整数,导弹数不超过 1000。

输入样例

1
389 207 155 300 299 170 158 65

输出样例

1
2
6
2

题解

求解这道题的关键在于下面这个性质 \[ 覆盖整个序列的最少的不上升子序列的个数 == 最长上升子序列的长度 \] 所以,这题的第二问可以转化为:求最长上升子序列的长度。

证明:AcWing 1010. 拦截导弹【附贪心证明】 - AcWing

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2000;

// a -- 高度
// dp1 -- [1,i]最大上升长度
// dp2 -- [1,i]最少需要的拦截系统
int a[maxn],dp1[maxn],dp2[maxn];
int n,res,cnt;

int main(void){
while(cin>>a[n+1]){n++;}
for(int i = 2 ; i <= n ; i ++){
for(int j = 1 ; j < i ; j ++){
if(a[i] <= a[j]) dp1[i] = max(dp1[i], dp1[j] + 1);
else dp2[i] = max(dp2[i], dp2[j]+1);
// 若a[i] > a[j], 说明需要一个新的拦截系统,才能容纳这个数。
// 所以dp2[i]需要和dp[j]+1取最值,进行状态更新
// 若a[i] <= a[j], 则之前的拦截系统就可以拦截这个数
// 所以dp2[i]只需要和dp[j]取最值,但dp[j]已经被cnt考虑过了,所以不需要再更新。
res = max(dp1[i],res);
cnt = max(dp2[i],cnt);
}
}
cout<<res+1<<"\n"<<cnt+1;
return 0;
}

187 导弹防御系统

为了对抗附近恶意国家的威胁,RR 国更新了他们的导弹防御系统。

一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。

例如,一套系统先后拦截了高度为 3 和高度为 4 的两发导弹,那么接下来该系统就只能拦截高度大于 4的导弹。

给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

输入格式

输入包含多组测试用例。

对于每个测试用例,第一行包含整数 n,表示来袭导弹数量。

第二行包含 n 个不同的整数,表示每个导弹的高度。

当输入测试用例 n=0 时,表示输入终止,且该用例无需处理。

输出格式

对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。

数据范围

1≤n≤501≤n≤50

输入样例

1
2
3
5
3 5 2 4 1
0

输出样例

1
2

样例解释

对于给出样例,最少需要两套防御系统。

一套击落高度为 3,43,4 的导弹,另一套击落高度为 5,2,15,2,1 的导弹。

题解

这题需要使用dfs求解。

首先,使用up和down数组分别保存当前所有的上升/下降序列的末尾值

然后,从d=1到d=n依次处理每个数,每次处理考虑四种情况:

1)插入已存在的上升序列;

2)创建并插入新的上升序列;

3)插入已存在的下降序列中;

4)创建一个新下降序列;

最后,使用全局的ans剪枝,考虑所有情况后,得到最优解。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100;

int n,ans;
int a[maxn],up[maxn],down[maxn];

// d - 处理到第几个数
// u - 当前需要多少个上升系统
// v - 当前需要多少个下降系统
void dfs(int d, int u, int v){
if(u+v >= ans) return;
if(d == n+1){
ans = min(ans, u+v);
return;
}
// 尝试放入上升系统中
// up数组是从大到小排序的,找到第一个<a[d]的即是最优的
int f = 0;
for(int i = 1 ; i <= u ; i++){
if(a[d] > up[i]){
f = i;
int t = up[i];
up[i] = a[d];
dfs(d+1,u,v);
up[i] = t;
break;
}
}
if(f == 0){
up[u+1] = a[d];
dfs(d+1,u+1,v);
}
// 尝试放入下降系统中,同理
f = 0;
for(int i = 1 ; i <= v ; i++){
if(a[d] < down[i]){
f = i;
int t = down[i];
down[i] = a[d];
dfs(d+1,u,v);
down[i] = t;
break;
}
}
if(f == 0){
down[v+1] = a[d];
dfs(d+1,u,v+1);
}
}

int main(void){
while(cin>>n, n){
for(int i = 1 ; i <= n ; i ++){
cin>>a[i];
}
ans = n;
dfs(1,0,0);
cout<<ans<<"\n";
}
return 0;
}

272 最长公共上升子序列

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。

小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。

小沐沐说,对于两个数列 A 和 B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。

不过,只要告诉奶牛它的长度就可以了。

数列 AA 和 BB 的长度均不超过 3000。

输入格式

第一行包含一个整数 N,表示数列 A,B 的长度。

第二行包含 N 个整数,表示数列 A。

第三行包含 N 个整数,表示数列 B。

输出格式

输出一个整数,表示最长公共上升子序列的长度。

数据范围

1≤N≤3000

序列中的数字均不超过 2^31−1。

输入样例

1
2
3
4
2 2 1 3
2 1 2 3

输出样例

1
2

题解

设dp[i][j]为:考虑从a的[1,i]区间 和 b的[1,j]的区间,且以b[j]结尾的LCIS长度

更新dp[i][j]策略如下:

从小到大遍历每一个i和j,判断a[i]是否等于b[j],

若a[i] != b[j],则a[i]对LCIS的长度没有贡献,dp[i][j] = dp[i-1][j];

若a[i] == b[j],则往前遍历所有dp[i][k](k < j && a[i] > b[k]),更新dp[i][j] 为 dp[i][k]+1的最大值;

最后,答案为dp[i][j]的最大值。

代码

O(N^3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3050;

int n,ans;
int a[maxn],b[maxn],dp[maxn][maxn];

int main(void){
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 ++){
if(a[i] != b[j]) dp[i][j] = dp[i-1][j];
else{
int maxv = 1;
// 注意到,这里的遍历是和i无关的,只和1-j有关
// 因此可以移到i层,进行优化
for(int k = 1 ; k <= j ; k ++){
if(a[i] > b[k]){
maxv = max(dp[i][k]+1, maxv);
}
}
dp[i][j] = max(dp[i][j], maxv);
}
ans = max(dp[i][j],ans);
}
}
cout<<ans;
return 0;
}

O(N^2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3050;

int n,ans;
int a[maxn],b[maxn],dp[maxn][maxn];

int main(void){
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;
for(int j = 1 ; j <= n ; j ++){
if(a[i] != b[j]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i][j], maxv);
if(a[i] > b[j]) maxv = max(maxv, dp[i - 1][j] + 1);
ans = max(dp[i][j],ans);
}
}
cout<<ans;
return 0;
}

AcWing算法提高课:动态规划2-最长上升子序列
https://czwcugb.github.io/题解/acwing-advance/AcWing-DP-2/
作者
ChenZhiwei
发布于
2025年2月3日
许可协议