1.基于LIS(最长上升子序列)模型的DP问题(算法提高课)

目录

一,怪盗基德的滑翔翼    (朴素LIS)

 二,登山   (朴素LIS)

三,合唱队形   (朴素LIS)

 四,友好城市  (朴素LIS)

五,最大上升子序列和  (朴素LIS)

六,拦截导弹   (贪心+LIS)

七,导弹防御系统

八,最长公共上升子序列


下面列举的所有题目都是基于LIS(最长上升子序列)衍生出来的题,都可以用LIS或LIS加上其他算法结合求解,难度依次递增

一,怪盗基德的滑翔翼    (朴素LIS)

题目描述

怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。

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

假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。初始时,怪盗基德可以在任何一幢建筑的顶端。他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)

Input

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

Output

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

Sample

InputcopyOutputcopy
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

题目意思大概就是,求任意一点的左边到这个点的最长上升子序列,和从这个点向右的最长下降子序列,找到其中之一的最大值,对于从这个点往右的最长下降子序列,我们就可以看成是从右往左到这个点的的最长上升子序列就可以了,可以直接套LIs的模板,可以用朴素做法也可以用二分优化的做法,取决于数据范围大小,不会LIS的可以先去看看我的另一篇文章

https://blog.csdn.net/m0_74911187/article/details/131888451?spm=1001.2014.3001.5501

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110;

int dp[N],a[N];

int main()
{
	int t;
	cin >> t;
	while (t--)
	{
		int n;
		cin >> n;
		for (int i = 1; i <= n; i++)
			cin >> a[i];
		int res = 0;
		for (int i = 1; i <= n; i++)//正向求一遍上升子序列
		{
			dp[i] = 1;
			for (int j = 1; j < i; j++)
				if(a[j]<a[i])
					dp[i] = max(dp[i], dp[j] + 1);
			res = max(res, dp[i]);
		}
		for (int i = n; i; i--)//反向求一遍上升子序列
		{
			dp[i] = 1;
			for (int j = n; j > i; j--)
				if(a[j]<a[i])
					dp[i] = max(dp[i], dp[j] + 1);
			res = max(res, dp[i]);
		}
		cout << res << endl;
	}
	return 0;
}

 二,登山   (朴素LIS)

题目描述

五一到了,PKU-ACM队组织大家去登山观光,队员们发现山上一个有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

Input

Line 1: N (2 <= N <= 1000) 景点数
Line 2: N个整数,每个景点的海拔

Output

最多能浏览的景点数

Sample

InputcopyOutputcopy
8
186 186 150 200 160 130 197 220
4

 题意大概就是求出一个点的从左到这个点的最长上升子序列和从这个点往后的最大上升子序列,二者加起来,对于所有的点求出最大值,与上面那个题不同的是,这个题需要将以这个点结尾和以这个点开头的最长上升子序列与最长下降子序列加起来的和

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int dp1[N],dp2[N],a[N];

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <= n; i++)//以这个点结尾的最长上升子序列
	{
		dp1[i] = 1;
		for (int j = 1; j < i; j++)
			if (a[j] < a[i])
				dp1[i] = max(dp1[i], dp1[j] + 1);
	}
	for (int i = n; i; i--)//以这个点为开头的最长下降子序列
	{
		dp2[i] = 1;
		for (int j = n; j > i; j--)
			if (a[j] < a[i])
				dp2[i] = max(dp2[i], dp2[j] + 1);
	}
	int res = 0;
	for (int i = 1; i <= n; i++)//求出以所有点为开头和结尾的最长上升子序列与最长下降子序列的和的最大值
		res = max(res, dp1[i] + dp2[i]-1);
	cout << res;
	return 0;
}

三,合唱队形   (朴素LIS)

Description

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

合唱队形是指这样的一种队形:设 k 位同学从左到右依次编号为 1,2,……,k,他们的身高分别为 t1​,t2​, … ,tk​,则他们的身高满足 t1​<⋯< ti ​>ti+1​> … >tk​(1≤i≤k)。

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

Input

共二行。

第一行是一个整数 n(2≤n≤100),表示同学的总数。

第二行有 n 个整数,用空格分隔,第 i 个整数 ti​(130≤ti​≤230)是第 i 位同学的身高(厘米)。

Output

一个整数,最少需要几位同学出列。

Sample 1

InputcopyOutputcopy
8
186 186 150 200 160 130 197 220
4

Hint

对于50% 的数据,保证有 n≤20。

对于全部的数据,保证有n≤100。

 这题与第二题类似,只需要最后用n减去我们得到的最大值就可以了

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110;

int dp1[N],dp2[N],a[N];

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <= n; i++)//以这个点结尾的最长上升子序列
	{
		dp1[i] = 1;
		for (int j = 1; j < i; j++)
			if (a[j] < a[i])
				dp1[i] = max(dp1[i], dp1[j] + 1);
	}
	for (int i = n; i; i--)//以这个点为开头的最长下降子序列
	{
		dp2[i] = 1;
		for (int j = n; j > i; j--)
			if (a[j] < a[i])
				dp2[i] = max(dp2[i], dp2[j] + 1);
	}
	int res = 0;
	for (int i = 1; i <= n; i++)//求出以所有点为开头和结尾的最长上升子序列与最长下降子序列的和的最大值
		res = max(res, dp1[i] + dp2[i]-1);
	cout << n-res;
	return 0;
}

 四,友好城市  (朴素LIS)

Description

有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航道不相交的情况下,被批准的申请尽量多。

Input

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

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

Output

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

Sample 1

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

Hint

50% 1<=N<=5000,0<=xi<=10000

100% 1<=N<=2e5,0<=xi<=1e6

 题目抽象出来就是下面这样

 两条线段之间的连线代表我们建的桥梁,由题意可以发现,对于任意合法的建桥方式,我们的自变量,所对应的因变量一定是严格上升的,因为如果不是上升的,如图所示,那么就不可能是一个合法的剑桥方式,因此我们求出来自变量所对应的因变量的最长上升子序列就是我们的合法建桥方式

需要注意的是,这题的数据范围比较大,所以我们求最长上升子序列时要用O(Nlog N)的做法,也就是二分优化的做法

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

typedef pair<int, int>pii;

const int N = 2e5 + 10;
pii a[N];
int len,q[N];

int main()
{
	int n;
	cin >> n;
	for (int i = 0; i < n; i++)
		cin >> a[i].first >> a[i].second;
	sort(a, a + n);
	int res = 0;
	for (int i = 0; i < n; i++)
	{
		int l = 0,r = len;
		while (l < r)
		{
			int mid = l + r +1>> 1;
			if (a[i].second > q[mid])l = mid;
			else
				r = mid - 1;
		}
		len = max(len, r+1);
		q[r+1] = a[i].second;
	}
	cout << len;
	return 0;
}

五,最大上升子序列和  (朴素LIS)

Description

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

Input

输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

Output

最大上升子序列和

Sample

InputcopyOutputcopy
7
1 7 3 5 9 4 8
18

 这题就不能直接套用LIS的模板了,但是我们可以用分析LIS的方法去分析这个题,我们以搭dp[i]表示以a[i]结尾的上升子序列的集合,集合的属性为和的最大值,那么我们接着看dp[i]可以由上面状态转移过来,我们以a[i]前的数将dp[i]分成若干个集合,集合种包含了0,a[1],a[2],a[3],……a[i-1],那么dp[i]=max(0,a[1],a[2],a[3],……,a[i-1])+a[i]

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int dp[N], a[N];

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

六,拦截导弹   (贪心+LIS)

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入格式

每个测试文件只包含一组测试数据,每组输入若干个整数,表示导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数)。
输出格式
对于每组输入数据,第一行输出这套系统最多能拦截多少导弹,第二行输出如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入样例 
389 207 155 300 299 170 158 65输出样例
6
2

 对于第一问,就是求最长不上升子序列,可以在LIS的基础上修改一下判断条件就可以了,主要研究第二问,第二问要求我们用最少的导弹系统拦截全部的导弹,因此我们要用到的贪心的思想:

从前往后扫描每个数,对于每个数,我们都会有两种情况,情况1:如果现有的导弹拦截系统的子序列的结尾都小于这个数,则创建新的子序列。情况2:将当前数放到结尾大于等于它的最小的子序列后面

用一个g数组来存储每个导弹拦截系统的结尾那个数,以这个贪心的思路,我们得到的g数组一定是严格单调不下降的,这样我们就可以用二分找到大于等于这个数的最小的那个序列,然后将他修改,我们会惊奇的发现这个做法与我们用二分优化后求最长上升子序列是一样的,所以我们求得最长上升子序列长度就是我们需要得导弹拦截系统,非常巧妙对吧

所以,最终我们只用求一遍最长不上升子序列和最长上升子序列就可以了

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int a[N], dp[N], g[N], len;
int n;

int main()
{
	while (cin >> a[n])n++;
	int res = 0;
	for (int i = 0; i < n; i++)
	{
		dp[i] = 1;
		for (int j = 0; j < i; j++)
			if (a[j] >= a[i])
				dp[i] = max(dp[i], dp[j] + 1);
		res = max(res, dp[i]);
	}
	for (int i = 0; i < n; i++)
	{
		int l = 0, r = len;
		while (l < r)
		{
			int mid = l + r + 1 >> 1;//因为下面判断得到的l=……,因为要加1
			if (a[i]>g[mid])//二分找到大于等于这个数最小的那个数
				l = mid;  //等同于找到小于这个数的最大那个数的后面一个数
			else
				r = mid - 1;
		}
		g[r + 1] = a[i];
		len = max(len, r + 1);
	}
	cout << res << endl << len;
	return 0;
}

七,导弹防御系统   (贪心+LIS+DFS)

题目描述

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

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

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

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

输入格式

输入包含多组测试用例。

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

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

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

输出格式

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

数据范围

1≤n≤50

输入样例:

5
3 5 2 4 1
0 

输出样例:

2

样例解释

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

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

 这个题与上面哪个导弹拦截系统很像,所以我们可以参考上面那题,有所不同的是,这个多了一种情况,即一个数既可以是上升子序列也可以是下降子序列中的数,因此相较于上面那个题我们就多了两种情况,即对于下降子序列我们是放还是新增一个子序列

同样是贪心的思路,这题我们要用暴搜dfs来实现,即对于每个数来说我们都搜索一遍,看是放在上升子序列中比较合适还是下降子序列中比较合适,我们用一个up数组来存储每个上升子序列的最后一个数,down数组存储每个下降子序列的最后一个数,上面已经说明了down数组是一个严格单调递增的,up数组以这个贪心思路得到的一定是一个严格单调递减的

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=55;

int n;
//up存的是每个上升子序列最后一个数的值,down存的是每个下降子序列的最后一个值,q是数组
int up[N],down[N],q[N];
int res;//res存储答案

//u表示枚举到哪个数的下标,su表示上升子序列的个数,du表示下降子序列的个数
void dfs(int u,int su,int du)
{
    //这题一定要及时剪枝,也就是当前上升子序列的个数加下降子序列的个数大于我们之前的得到的最小解时
    //就可以return了,减少不必要的运行时间
    if(su+du>=res)return;
    if(u==n)//如果能运行到这一步,说明我们枚举完了所有点并且能找到一个更优解
    {
        res=su+du;
        return;
    }
    
    //情况1,将这个数放到上升子序列中
    int k=0;
    while(k<su&&up[k]>=q[u])k++;//找到上升子序列中比这个数小中的最大子序列
    int t=up[k];//将这个up[k]存下来,搜索完要恢复现场
    up[k]=q[u];//将这个数加到第k个子序列后面
    if(k<su)
        dfs(u+1,su,du);//能将这个数加到已有的上升子序列中后面
    else
        dfs(u+1,su+1,du);//现有的子序列没有能让这个数加进去,只能新建一个上升子序列
    up[k]=t;//恢复现场
    
    //情况2:将这个数放到下降子序列中
    k=0;
    while(k<du&&down[k]<=q[u])k++;//找到下降子序列中比这个数大的最小子序列
    t=down[k];//将这个down[k]存下来,搜完完要恢复现场
    down[k]=q[u];//将这个数加到第k个子序列后面
    if(k<du)
        dfs(u+1,su,du);//能将这个数能加到现有的子序列中
    else
        dfs(u+1,su,du+1);//如果现有的子序列中没有能让这个数加入进去的,就新建一个上升子序列
    down[k]=t;//恢复现场
}
int main()
{
    while(cin>>n,n)
    {
        for(int i=0;i<n;i++)
            cin>>q[i];
        res=n;//首先要将答案初始化成最大,表示最坏情况需要n个导弹拦截系统
        dfs(0,0,0);
        
        cout<<res<<endl;
    }
    return 0;
}

八,最长公共上升子序列  (LIS+LCS)

题目描述

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

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

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

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

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

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

输入格式

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

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

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

输出格式

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

数据范围

1≤N≤3000,序列中的数字均不超过 2^31-1。

输入样例:

4
2 2 1 3
2 1 2 3

输出样例:

2

 这个题其实是最长上升子序列与最长公共子序列的结合

我们以f[i][j]代表所有a[1 ~ i]和b[1 ~ j]中以b[j]结尾的公共上升子序列的集合;集合的属性为最大值

我们以a的最后一个数a[i]是否在子序列中来划分f[i][j]

1,如果a[i]不在子序列中,那么f[i][j]=f[i-1][j]

2,如果a[i]在子序列中,那么f[i][j]=max(f[i][j],f[i-1][k]+1),k=1~j-1

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 3010;

int n;
int a[N], b[N];
int f[N][N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++)scanf("%d", &b[i]);;

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            f[i][j] = f[i - 1][j];//如果a[i]!=b[j]
            if (a[i] == b[j])
            {
                f[i][j] = max(f[i][j], 1);//如果a[i]==b[j],那么f[i][j]最小长度是1
                for (int k = 1; k < j; k++)//找到b[j]的上升子序列
                    if (b[j] > b[k])
                        f[i][j] = max(f[i][j], f[i-1][k] + 1);//满足条件的可以进行状态转移
            }
        }
    }
    int res = 0;
    for (int i = 1; i <= n; i++)//最长的公共上升子序列不一定是以b[n]结尾的,所以需要全部遍历一遍
        res = max(res, f[n][i]);
    cout << res;
    return 0;
}

我们可以发现这个方法是O(N^3)的,会超时,那么如何来优化呢?

只有在a[i]==b[j]时才会进入第三重循环,第三重循环求得其实就是在b序列中找到小于b[j]得最长公共上升子序列,然后在此基础上加1就可以得到f[i][j],但是在i固定时,我们对于每个j,如果他满足a[i]==b[j]他都进入第三重循环,进行重复得寻找最大值得过程,会做很多重复工作,所以我们可以在这一步上进行优化,我们用一个maxv来记录b[j]前得最长得公共上升子序列得最大值,这样,maxv是满足a[i] > b[k]的f[i - 1][k] + 1的前缀最大值。

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;

const int N=3010;

int n;
int a[N],b[N];
int f[N][N];

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)scanf("%d",&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(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]);
    cout<<res;
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值