最长上升子序列模型

供自己学习,欢迎讨论。
在这里插入图片描述


怪盗基德的滑翔翼

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

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

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

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

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

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

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

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

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

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

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

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

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

数据范围
1 ≤ K ≤ 100,
1 ≤ N ≤ 100,
0 < h < 10000

输入样例:

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

输出样例:

6
6
9

首先,求一个高度递减的楼房子序列长度最大,其实就是求一个最长下降子序列

然后,这个怪盗基德老哥可以选择任意楼房作为起始位置,接着选择一个方向飞到尽头

于是,我们可以画出如下三种情况:
在这里插入图片描述
对于左边界情况来说,其实就是中间位置的左侧序列长度为 0 的情况;右边界情况同理

所以,我们只需讨论中间情况即可(两侧边界情况是该情况的子集)

于是,对于任意位置 xx,我们分别需要求出以他为右端点最长上升子序列,以及作为左端点最长下降子序列

DP中经典的最长上升子序列模型f[i]的状态表示就是以i为端点的最长上升子序列

由此我们通过线性DP,可以求出任一点的左侧最长上升右侧最长下降

两者取一个 Max,就是以该点作为起点最佳飞行方向最大长度

然后再枚举所有点取一个 Max,就是最佳起点最大长度,便是本题的答案

这题的DP模型就是经典的最长上升子序列

在这里插入图片描述
在这里插入图片描述

#include <iostream>
using namespace std;
const int N = 10010;
int k;
int main()
{
	cin >> k;
	while (k -- ) {
		int n, res = 0;
		int a[N], mi[N], mx[N];
		cin >> n;
		for (int i = 1; i <= n; i ++ ) cin >> a[i];
		for (int i = 1; i <= n; i ++ ) {
			mx[i] = mi[i] = 1;
			for (int j = 1; j < i; j ++ ) {
				// 降序
				if (a[j] > a[i]) {
					mi[i] = max(mi[i], mi[j] + 1);	
					// 最高点(最大值)有可能在中间
					res = max(res, mi[i]);
				} 
				// 升序
				if (a[j] < a[i]) {
					mx[i] = max(mx[i], mx[j] + 1);	
					res = max(res, mx[i]);
				} 	
			}
		}
		cout << res << endl; 
	}
	return 0;
}
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n;
int h[N];
int f[N];

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d", &n);
        for (int i = 0; i < n; i ++ ) scanf("%d", &h[i]);

        int res = 0;
        for (int i = 0; i < n; i ++ )
        {
            f[i] = 1;
            for (int j = 0; j < i; j ++ )
                if (h[i] < h[j])
                    f[i] = max(f[i], f[j] + 1);
            res = max(res, f[i]);
        }

        memset(f, 0, sizeof f);
        for (int i = n - 1; i >= 0; i -- )
        {
            f[i] = 1;
            for (int j = n - 1; j > i; j -- )
                if (h[i] < h[j])
                    f[i] = max(f[i], f[j] + 1);
            res = max(res, f[i]);
        }

        printf("%d\n", res);
    }

    return 0;
}

笔记学习:
作者:彩色铅笔
链接:https://www.acwing.com/solution/content/51431/
来源:AcWing
代码学习:
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/112799/
来源:AcWing


登山

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

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

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

输入格式
第一行包含整数N,表示景点数量。

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

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

数据范围
2 ≤ N ≤ 1000

输入样例:

8
186 186 150 200 160 130 197 220

输出样例:

4

在这里插入图片描述
和上题一样存在边界情况,即最优解答案可以是一个单调下降子序列单调上升子序列

这种边界情况一样是先上升后下降的最长子序列子集,因此不用额外讨论

枚举每个点为中间点,左边下降,右边也是下降的最大长度。

从左边开始,往右边走的对于每个点的最大上升子序列最大值。
从右边开始,往左边走的对于每个点的最大上升子序列最大值。

最后遍历每个点,对于每个点作为中间的点,最大的值是 f[i] + g[i] - 1;

时间复杂度O(N2),空间复杂度O(N)

更好的解法

#include <iostream>
using namespace std;
const int N = 1010;
int n, res = 0;
int a[N], mx[N], mi[N];
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i ++ ) cin >> a[i];
	// 从前到后最长上升子序列
	for (int i = 1; i <= n; i ++ ) {
		mx[i] = 1;
		for (int j = 1; j < i; j ++ )
			if (a[j] < a[i]) 
				mx[i] = max(mx[i], mx[j] + 1);				
	}
	// 从后往前最长下降子序列
	for (int i = n; i >= 1; i -- ) {
		mi[i] = 1;
		for (int j = n; j > i; j -- ) 
			if (a[i] > a[j]) 
				mi[i] = max(mi[i], mi[j] + 1);										
	}
	for (int i = 1; i <= n; i ++ ) res = max(res, mx[i] + mi[i] - 1);
	cout << res;
	return 0;
}

合唱队形

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

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

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

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

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

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

数据范围
2 ≤ N ≤ 100,
130 ≤ Ti ≤ 230

输入样例:

8
186 186 150 200 160 130 197 220

输出样例:

4

算法
(线性DP,最长上升子序列) O(n2)
假设最优解的中心是第 i 个人,则 T1,T2,…,Ti 一定是以 Ti 结尾的最长上升子序列。
同理,TK,TK−1,…,Ti 也一定是以 Ti 结尾的最长上升子序列。

因此可以先预处理出:

  1. 从前往后以每个点结尾的最长上升子序列长度 f[i]
  2. 从后往前以每个点结尾的最长上升子序列长度 g[i]

那么以 k 为中心的最大长度就是 f[k] + g[k] - 1,遍历 k = 1, 2, …, n 取最大值即为答案。

求最长上升子序列问题(LIS)可以参考 AcWing 895. 最长上升子序列

时间复杂度
本题数据范围只有 100,因此可以用朴素的LIS求解方式,时间复杂度是 O(n2),使用贪心 + 二分可以将时间复杂度优化到 O(nlogn),具体可以参考 AcWing 896. 最长上升子序列 II

#include <iostream>
using namespace std;
const int N = 110;
int n, res;
int a[N], in[N], de[N];
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i ++ ) cin >> a[i];
	for (int i = 1; i <= n; i ++ ) {
		in[i] = 1;
		for (int j = 1; j < i; j ++ ) 
			if (a[j] < a[i])
				in[i] = max(in[i], in[j] + 1);
	}
	for (int i = n; i >= 1; i -- ) {
		de[i] = 1;
		for (int j = n; j > i; j -- ) 
			if (a[j] < a[i])
				de[i] = max(de[i], de[j] + 1);
	}
	for (int i = 1; i <= n; i ++ ) res = max(res, in[i] + de[i] - 1);
	cout << n - res;
	return 0;
}

笔记学习:
作者:yxc
链接:https://www.acwing.com/solution/content/3805/
来源:AcWing
更好的解法


友好城市

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

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

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

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

输入格式
第1行,一个整数N,表示城市数。

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

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

数据范围
1 ≤ N ≤ 5000,
0 ≤ xi ≤ 10000

输入样例:

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

输出样例:

4

在这里插入图片描述
我们可以发现,在两座桥不交叉的情况下,有a1<a2, b1<b2,在两座桥相交的情况下,有a1<a2, b2<b1

所以我们可以发现两桥是否交叉是可以用北岸城市编号的大小关系给反应出来的。

考虑一个枚举方案,按照上岸的坐标从小到大来枚举,然后我们只需关心下岸的坐标之间有何关系即可。

于是,可以轻易发现,上坐标从小到大枚举选择到的桥,其对应下坐标也必然是从小到大的。

该方案中,在上坐标排序的情况下,下坐标次序不是从小到大的,则必然不合法(会有相交)。

所以我们的思路就可以转化为以南岸的城市编号为基准,把所有的城市对从小到大排序,从而得到关于北岸城市的一个数组。因为当bi<bj时能成功建一座桥,所以问题就转化成了求取这个数组的最长上升子序列。

于是,这题就变成了:桥以上坐标从小到大排序后,找出下坐标的最长上升子序列长度

到此,我们对北方城市的数组做一次 LIS 就可以得到答案了。

复杂度分析
时间复杂度: O(N2)

// 错误代码,思路不严谨,还没考虑清楚哪里出问题了。
#include <iostream>
using namespace std;
const int N = 5010;
int n, res;
int no[N], so[N];
int f[N];
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i ++ ) cin >> no[i] >> so[i];
	for (int i = 1; i <= n; i ++ ) {
		f[i] = 1;
		for (int j = 1; j < i; j ++ ) 
			if (no[j] < no[i] && so[j] < so[i])
				f[i] = max(f[i], f[j] + 1);
	}
	for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);
	cout << res;
	return 0;
}
// 正确答案
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 5010;
int n, res;
PII a[N];
int f[N];
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i ++ ) cin >> a[i].first >> a[i].second;
	sort(a + 1, a + n + 1);
	for (int i = 1; i <= n; i ++ ) {
		f[i] = 1;
		for (int j = 1; j < i; j ++ ) 
			if (a[j].second < a[i].second)
				f[i] = max(f[i], f[j] + 1);
		res = max(res, f[i]);
	}
	cout << res;
	return 0;
}

最大上升子序列和

一个数的序列 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

输入样例:

7
1 7 3 5 9 4 8

输出样例:

18

(线性DP) O(n2)

状态表示:
f[i] 表示前i个数,以第i个元素结尾的最大上升子序列的和

状态属性:
最大上升子序列和最大 Max

状态转移:
对于每一个小于a[i]的a[j] (j < i)

f[i] = max(f[i], f[j] + a[i])

集合划分:
在这里插入图片描述
时间复杂度
状态总个数等于数列总长度N, 计算每一个状态需要枚举前i个数,所以总复杂度为O(n2)

#include <iostream>
using namespace std;
const int N = 1010;
int n, res;
int a[N], f[N];
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];
		for (int j = 1; j < i; j ++ ) 
			if (a[j] < a[i])
				f[i] = max(f[i], f[j] + a[i]);	
		res = max(res, f[i]);
	}
	cout << res;
	return 0;
} 

拦截导弹

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

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

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

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

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

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

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

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

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

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2

最长上升子序列
最长上升子序列 ||

DP + 贪心

第一问:最长不下降子序列长度,不再赘述。

第二问:能覆盖整个序列的最少的不上升子序列的个数

贪心:
用如下方式维护数组s,数组长度cnt,意为cnt个不下降子序列。保存的是每一个不上升子序列中的最后一个数:
遍历原序列,对于遍历到的每一个数x:

  1. 若x大于s中每一个数,则新建一个不上升子序列,放入x;
  2. 否则,找到s中大于等于x的最小的数,将其替换。
    由于s每次增加长度时,增加的数必然大于其前面s中的任何一个数;且每次替换时,不改变x与被替换数左右相邻两数的相对大小关系,故s必然维持单调递增。则s即为原序列的最长上升子序列。

即证明了:
“能覆盖整个序列的最少的不上升子序列的个数”等价于“该序列的最长上升子序列长度”
同理即有:
“能覆盖整个序列的最少的不下降子序列的个数”等价于“该序列的最长下降子序列长度”
(欲深究可研读离散数学中的Dilworth定理

下面证明上述贪心算法的正确性:
设A为用该贪心算法得到的方案,B为最优解方案。记A的不上升子序列个数为la,B的不上升子序列个数为lb。
显然lb<=la(否则B就不是最优解)。
故只需证明la>=lb。用调整法(微调法)。
若A=B,则la=lb,显然成立。
否则(即A!=B),必然可以找到两方案中第一个(以原序列的顺序)不同之处(某数放在了不同的位置,且原序列中该数前的所有数在两方案中放的位置皆相同),将该数在两方案中所处的位置上的数(即该数本身)以及两位置之后方案中的序列相互对调,结果所得方案仍为合法解。即我们将贪心算法所得方案调整成了最优方案,且在调整过程中,方案的序列数没有增加,故la>=lb(la -> lb,且la转变为lb的过程中la的大小没有增加)。

多种题解
优质贪心证明
更优解:使用stringstream

#include <iostream>
using namespace std;
const int N = 1010;
int n, a[N], res, cnt;
int f[N], g[N];
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[j] >= a[i])
				f[i] = max(f[i], f[j] + 1);
		res = max(res, f[i]);
	}
	cout << res << endl;		
	for (int i = 0; i < n; i ++ ) {
		// k子序列下标 
		int k = 0;
		// a[i]小于当前全部子序列结尾数g[k] 
		while (g[k] < a[i] && k < cnt) k ++ ; 
		// 1.用a[i]替换第k个子序列结尾的数字
		// 2.追加新的子序列 
		g[k] = a[i];
		// 如果没有大于等于a[i]的子序列结尾,则追加新的子序列 
		if (k == cnt) cnt ++ ;
	}
	cout << cnt; 
	return 0;
}

笔记学习:
作者:玖梦づ
链接:https://www.acwing.com/solution/content/6746/
来源:AcWing


导弹防御系统

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

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

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

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

输入格式
输入包含多组测试用例。

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

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

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

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

数据范围
1 ≤ n ≤ 50

输入样例:

5
3 5 2 4 1
0 

输出样例:

2

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

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

没懂,对于搜索的理解没有很透彻

算法
(DFS,迭代加深,剪枝,贪心) O(n2n)
为了能遍历所有情况,我们首先考虑搜索顺序是什么。

搜索顺序分为两个阶段:

  • 从前往后枚举每颗导弹属于某个上升子序列,还是下降子序列;
  • 如果属于上升子序列,则枚举属于哪个上升子序列(包括新开一个上升子序列);如果属于下降子序列,可以类似处理。

因此可以仿照AcWing 896. 最长上升子序列 II,分别记录当前每个上升子序列的末尾数up[],和下降子序列的末尾数down[]。这样在枚举时可以快速判断当前数是否可以接在某个序列的后面。

注意这里的记录方式和上一题稍有不同:

  • 这里是记录每个子序列末尾的数;
  • 上一题是记录每种长度的子序列的末尾最小值。

此时搜索空间仍然很大,因此该如何剪枝呢?

对于第二阶段的枚举,我们可以仿照上一题的贪心方式,对于上升子序列而言,我们将当前数接在最大的数后面,一定不会比接在其他数列后面更差。
这是因为处理完当前数后,一定出现一个以当前数结尾的子序列,这是固定不变的,那么此时其他子序列的末尾数越小越好。

注意到按照这种贪心思路,up[]数组和down[]数组一定是单调的,因此在遍历时找到第一个满足的序列后就可以直接break了。

最后还需要考虑如何求最小值。因为DFS和BFS不同,第一次搜索到的节点,不一定是步数最短的节点,所以需要进行额外处理。
一般有两种处理方式:

  • 记录全局最小值,不断更新; 这种搜索顺序可以参考一瞬流年丶涅槃 同学的题解
  • 迭代加深。一般平均答案深度较低时可以采用这种方式。后面的代码中采用的是这种方式。

时间复杂度
每个数在第一搜索阶段有两种选择,在第二搜索阶段只有一种选择,但遍历up[]和down[]数组需要 O(n) 的计算量,因此总时间复杂度是 O(n2n)。

#include <iostream>
using namespace std;
const int N = 60;
int n;
int h[N];
int up[N], down[N];
bool dfs(int depth, int u, int su, int sd)
{
	// 如果上升序列个数 + 下降序列个数 > 总个数上限,则回溯 
	if (su + sd > depth) return false;
	if (u == n) return true;
	// 枚举放在上升子序列中的情况
	bool flag = false;
	for (int i = 1; i <= su; i ++ ) 
		if (up[i] < h[u]) {
			int t = up[i];
			up[i] = h[u];
			if (dfs(depth, u + 1, su, sd)) return true;
			up[i] = t;
			flag = true;
			break; // 注意由上述证明的贪心原理,只要找到第一个可以放的序列,就可以结束循环了
		}
	if (!flag) { // 如果不能放到任意一个序列后面,则单开一个新的序列
		up[su + 1] = h[u];
		if (dfs(depth, u + 1, su + 1, sd)) return true;
	}
	// 枚举放在下降子序列中的情况 
	flag = false;
	for (int i = 1; i <= sd; i ++ ) 
		if (down[i] > h[u]) {
			int t = down[i];
			down[i] = h[u];
			if (dfs(depth, u + 1, su, sd)) return true;
			down[i] = t;
			flag = true;
			break;
		}
	if (!flag) {
		down[sd + 1] = h[u];
		if (dfs(depth, u + 1, su, sd + 1)) return true;
	}
	return false;
}
int main()
{
	while (cin >> n, n) {
		for (int i = 0; i < n; i ++ ) cin >> h[i];
		int depth = 0;
		while (!dfs(depth, 0, 0, 0)) depth ++ ; // 迭代加深搜索
		cout << depth << endl;
 	}
	return 0;
}

笔记、代码学习:
作者:yxc
链接:https://www.acwing.com/solution/content/4258/
来源:AcWing

一瞬流年丶涅槃 同学的题解:
导弹防御系统很自然的想到LIS算法,不过这里的条件是一套防御系统的导弹拦截高度要么一直上升要么一直下降,所以用LIS算法是不正确的

而LIS中,最核心的思想在于能否将一个元素加入到序列中,只与这个序列目前的最后一个元素有关
这道题就用了这个关键的思想。
用up[k]和down[k]记录第k套上升(下降)系统目前所拦截的最后一个导弹
dfs(u,v,t)意味着已有u个上升,v个下降,正在处理第t个数

按理说,每拿到一个新的数字应该将它所有能放入的序列都放一遍的
但扩展节点时却存在一个贪心策略,大大节省了时间。
假设现在要把一个数放入一个上升序列,那么一定是所有能放入的上升序列中,最后一个元素最大的那一个。
其实想想也是,既然每个数字都要放到一个序列中,
对于上升序列,肯定是目前越小越有用,既然能放入大的里面,何必浪费一个小的呢
注意到其实up[i]按这种策略已经是排好序的了,所以只用找最先碰到的一个就行了

#include<iostream>
using namespace std;
const int N = 55;
int a[N], ans, up[N], down[N], n;
void dfs(int u, int d, int t)  //u表示上升的系统个数,d表示下降的系统个数,t表示第t个数
{
    if(u + d >= ans) return ;
    if(t ==  n){
        if(u + d < ans)ans = u + d;
        return ;
    }
    int i;
    for(i = 1; i <= u; i++)  //找到第一个末尾数小于a[t]的导弹系统
        if(up[i] < a[t])break;

    int temp = up[i];
    up[i] = a[t];//添加到该导弹系统中
    dfs(max(u, i), d, t + 1);
    up[i] = temp;  //恢复现场
    for(i = 1; i <= d; i++)//找到第一个末尾数大于a[t]的导弹系统
        if(down[i] > a[t])break;
    temp = down[i];
    down[i] = a[t];//添加到该导弹系统中去
    dfs(u, max(d, i), t + 1);
    down[i] = temp;//恢复现场
}
int main()
{
    while(scanf("%d", &n) != EOF && n != 0){
        ans = 100;
        for(int i = 0; i < n; i++)
            cin >> a[i];
        dfs(0, 0, 0);
        printf("%d\n", ans);
    }
    return 0;
}

笔记、代码学习:
作者:一瞬流年丶涅槃
链接:https://www.acwing.com/solution/acwing/content/4010/
来源:AcWing


最长公共上升子序列

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

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

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

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

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

数列 A 和 B 的长度均不超过 3000。

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

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

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

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

数据范围
1 ≤ N ≤ 3000,序列中的数字均不超过 231−1。

输入样例:

4
2 2 1 3
2 1 2 3

输出样例:

2

(DP,线性DP,前缀和) O(n2)

这道题目是最长上升子序列最长公共子序列的结合版,在状态表示和状态计算上都是融合了这两道题目的方法。

状态表示:

  • f[i][j]代表所有a[1 ~ i]和b[1 ~ j]中以b[j]结尾的公共上升子序列的集合;
  • f[i][j]的值等于该集合的子序列中长度的最大值;

状态计算(对应集合划分):

首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:

  • 不包含a[i]的子集,最大值是f[i - 1][j];
  • 包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
    • 倒数第二个数是0,序列只包含b[j]一个数,长度是1;
    • 子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;
    • 子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[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);
        }
    }
}

然后我们发现每次循环求得的maxv是满足a[i] > b[k]的f[i - 1][k] + 1的前缀最大值。
因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。

最终答案枚举子序列结尾取最大值即可。

时间复杂度

代码中一共两重循环,因此时间复杂度是 O(n2)。

// 朴素做法 O(n^3)
#include <iostream>
using namespace std;
const int N = 3010;
int n;
int a[N], b[N];
int f[N][N];
int main()
{
	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 ++ ) {
			f[i][j] = f[i - 1][j];
			if (a[i] == b[j]) {
				int maxv = 1;
				for (int k = 1; k < j; k ++ )
					if (b[k] < b[j])
						maxv = max(maxv, f[i - 1][k] + 1);
				f[i][j] = max(maxv, f[i][j]);
			}
		}
	int res = 0;
	for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
	cout << res;
	return 0;
}
// 优化后
#include <iostream>
using namespace std;
const int N = 3010;
int n;
int a[N], b[N];
int f[N][N];
int main()
{
	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 ++ ) {
			f[i][j] = f[i - 1][j];
			if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
			if (b[j] < a[i]) maxv = max(maxv, f[i - 1][j] + 1);
		}	
	}
	int res = 0;
	for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
	cout << res;
	return 0;
}

笔记、代码学习:
作者:yxc
链接:https://www.acwing.com/solution/content/4955/
来源:AcWing

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值