专题四:最长上升子序列
/*
Name:动态规划专题之最长上升子序列
Author:巧若拙
Description:1759_最长上升子序列
描述:一个数的序列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)等等。这些子序列中最长的长度是4,比如子序列(1, 3,5, 8).
你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入
输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。
输出:最长上升子序列的长度
样例输入:
7
1 7 3 5 9 4 8
样例输出
4
*/
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX = 1001;
int A[MAX];
int S1[MAX]; //记录到元素i为止的最长上升子序列的长度
int S2[MAX]; //记录到元素i为止的最长上升子序列的长度
int S3[MAX]; //记录到元素i为止的最长上升子序列的长度
int S4[MAX]; //记录到元素i为止的最长上升子序列的长度
int S5[MAX+1]; //记录最长上升子序列,下标从1开始
int DP_1(int i); //记忆化搜索
int DP_2(int n); //与记忆化搜索相对应的动态规划算法
int DP_3(int n); //动态规划:逆序搜索,返回最长上升子序列第一个元素的下标
int DP_4(int n); //动态规划:逆序搜索,返回最长上升子序列长度
int DP_5(int n); //顺序处理,二分插入
int Pos(int low, int high, int x);//二分查找,返回第一个比x大的元素下标
void Print(int len, int i);//递归输出子序列
void Print2(int n); //输出序列
void Print3(int n); //输出序列
int main()
{
intn;
cin>> n;
for(int i=0; i<n; i++)
{
cin>> A[i];
}
intmaxLen = DP_1(n-1);//记忆化搜索,需要用到全局变量A[MAX],另有S1[MAX]初始化为0。
for(int i=n-2; i>=0; i--)//递减比递增效率应该要高些
{
if(maxLen < S1[i])
maxLen= S1[i];
}
cout<< maxLen << endl;
Print(maxLen,n-1);
cout<< endl;
cout<< DP_2(n) << endl;//顺序处理,需要用到全局变量A[MAX],另有S2[MAX]初始化为0。
intpos = DP_3(n);//逆序处理,返回最长上升子序列第一个元素的下标,需要用到全局变量A[MAX],另有S3[MAX]初始化为0。
cout<< S3[pos] << endl;
Print2(n);
Print3(n);
cout<< DP_4(n) << endl;//逆序处理,返回最长上升子序列长度,需要用到全局变量A[MAX],另有S4[MAX]初始化为0。
cout<< DP_5(n) << endl;//顺序处理,二分插入,需要用到全局变量A[MAX],另有S4[MAX]初始化为0。
return 0;
}
算法1:记忆化搜索,需要用到全局变量A[MAX],另有S1[MAX]初始化为0。
int DP_1(int i)
{
if(S1[i] != 0)
return //语句1
if(i == 0)
{
S1[i]= //语句2
}
else
{
intlen = 0;
for(int j=i-1; j>=0; j--)
{
if(len < DP_1(j) && A[i] > A[j]) //语句3
len= DP_1(j);
}
S1[i]= len + 1;
}
returnS1[i];
}
问题1:将语句1和语句2补充完整。
问题2:根据样例输入:1 7 3 5 9 4 8,写出对应数组S1[]的值。
问题3:语句3能否改为if (A[i] > A[j] && len < DP_1(j))?:
参考答案:
问题1:语句1:return S1[i]; 语句2:S1[i] = 1;
问题2:数组S1[]的值为:1 2 2 3 4 3 4。
问题3:不能。语句3中需要先递归计算出子问题的解,再判断是否得到满足条件的子序列,若先判断A[i]> A[j],则该条件不成立时,永远无法调用递归函数计算子问题的解。
算法2:与记忆化搜索相对应的动态规划算法,需要用到全局变量A[MAX],另有S2[MAX]初始化为0。
int DP_2(int n)
{
for(int i=0; i<n; i++)
{
intlen = 0;
for(int j=i-1; j>=0; j--) //语句1
{
if(A[i] > A[j] && len < S2[j])
len= S2[j];
}
S2[i]= len + 1;
}
intmaxLen = S2[n-1]; //记录最长上升子序列的长度
for(int i=n-2; i>=0; i--)
{
if(maxLen < S2[i])
maxLen= S2[i];
}
returnmaxLen;
}
问题1:根据样例输入:1 7 3 5 9 4 8,写出对应数组S2[]的值。
问题2:语句1能否改为:for (int j=0; j<i; j++) ?哪种写法效率更高?
问题3:若把题目改为:求出最长不下降子序列的长度。该如何修改DP_2的代码?
参考答案:
问题1:数组S2[]的值为:1 2 2 3 4 3 4。
问题2:可以。因为语句1所在循环体的作用是在A[0...i-1]中,找出一个比A[i]小且最长的上升子序列,故顺序查找和逆序查找均可。但是对于上升子序列来说,S2[j]的值是递增的,逆序查找能更快地找到最大的S2[j],故语句1的写法效率更高。
问题3:把语句if (A[i] > A[j] &&S2[j] > S2[i])改为if (A[i]>= A[j] && S2[j] > S2[i])即可。
算法3:动态规划,逆序处理,需要用到全局变量A[MAX],另有S3[MAX]初始化为0。
int DP_3(int n) //动态规划:逆序搜索,返回最长上升子序列第一个元素的下标
{
intpos = n - 1; //记录最长上升子序列最后一个元素的下标
for(int i=n-1; i>=0; i--)
{
intlen = 0; //记录A[i]的后继子序列的长度
for(int j=i+1; j<n; j++)
{
if(A[j] > A[i] && S3[j] > len)
len= S3[j];
}
S3[i]= len + 1;
if(S3[i] > S3[pos])
pos= i;
}
returnpos;
}
问题1:根据样例输入:1 7 3 5 9 4 8,写出对应数组S3[]的值。
问题2:修改DP_3(),使其返回最长上升子序列的长度。
问题3:DP_3()返回的不是最长上升子序列的长度,而是其首个元素的下标,目的是为了方便输出该最长上升子序列,请在DP_3()的基础上,编写一段代码,输出该最长上升子序列。
参考答案:
问题1:数组S3[]的值为:4 2 3 2 1 2 1。
问题2:代码如下:
算法4:动态规划,逆序处理,需要用到全局变量A[MAX],另有S4[MAX]初始化为0。
int DP_4(int n) //逆序搜索,返回最长上升子序列长度
{
intmaxLen = 0; //记录最长上升子序列的长度
for(int i=n-1; i>=0; i--)
{
for(int j=i+1; j<n; j++)//在A[i]后面的元素中查找最大的S4[j]
{
if(A[j] > A[i] && S4[j] > S4[i])
S4[i] = S4[j];
}
S4[i]++;
if(maxLen < S4[i])
maxLen= S4[i];
}
returnmaxLen;
}
问题3:有两种实现方式:
void Print2(int n)
{
intpos = DP_3(n);
intlen = S3[pos]; //子序列的长度
for(int i=pos; len>0; i++) //总共输出len个元素
{
if(S3[i] == len)
{
cout<< A[i] << " ";
len--;
}
}
cout<< endl;
}
void Print3(int n)
{
intpos = DP_3(n);
inti, j;
for(i=pos; i<n;) //总共输出len个元素
{
cout<< A[i] << " ";
for(j=i+1; j<n; j++)
{
if(S3[j] == S3[i]-1)
break;
}
i= j;
}
cout<< endl;
}
算法5:顺序处理,二分插入,需要用到全局变量A[MAX],另有S5[MAX+1]初始化为0。
int DP_5(int n) //顺序搜索,二分插入
{
intm = 0; //记录最长不下降子序列的长度
S5[++m]= A[0];
for(int i=1; i<n; i++)
{
if(A[i] > S5[m])
{
S5[++m]= A[i];
}
else
{
S5[Pos(1,m-1, A[i])] = A[i]; //语句1
}
}
returnm;
}
int Pos(int low, int high, int x)//二分查找,返回第一个比x大的元素下标
{
intmid;
while(low <= high)
{
mid= (low + high)/2;
if(S5[mid] > x)
{
high= //语句2
}
else
{
low= //语句3
}
}
return //语句4
}
问题1:分别根据样例输入:1 7 3 5 9 4 8 和 1 7 3 5 9 2 8,写出对应数组S5[]的值。
问题2:语句1能否改为:S5[Pos(0, m, A[i])] = A[i];?为什么?
问题3:将语句2,语句3和语句4补充完整。
参考答案:
问题1:数组S5[]的值为:1 3 4 8 和 1 2 5 8。由此我们可以看到,算法5虽然能够获得最长子序列的长度,但是不一定能获得正确的子序列。
问题2:不能。因为数组S5的下标从1开始,故不能写作S5[Pos(0, m, A[i])] = A[i];虽然可以写作S5[Pos(1, m, A[i])] = A[i];但是因为我们已经知道A[i] > S5[m],故在S5[1...m-1]中二分查找第一个比x大的元素下标即可,因此语句1的写法更好。
问题3:语句2:high = mid - 1; 语句3:low = mid +1; 语句4:return low;
拓展练习:
原题只要求计算出最长上升子序列的长度,并未要求输出该最长上升子序列,在算法3的问题3中,我们要求在S[3]的基础上输出该最长上升子序列。现在要求在算法1或算法2的基础上,编写函数void Print(int len, int i)//递归输出子序列。
参考答案:
void Print(int len, int i)//递归输出子序列
{
if(len == 0)
return;
while(S1[i] != len) //不能写S1[i]<len,因为有可能出现长度相同的子序列
{
i--;
}
Print(len-1,i-1);
cout<< A[i] << " ";
}
课后练习:
练习1:1044_拦截导弹 1999年NOIP全国联赛提高组
题目描述:某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入描述 InputDescription
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数)
输出描述 OutputDescription
输出这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
样例输入 SampleInput
389 207 155 300 299 170 158 65
样例输出 SampleOutput
6(最多拦截导弹数)
2(要拦截所有导弹最少要配备的系统数)
数据范围及提示 DataSize & Hint
导弹的高度<=30000,导弹个数<=20
练习2:3532_最大上升子序列和
描述:一个数的序列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 (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。
输出
最大上升子序列和
样例输入
7
1 7 3 5 9 4 8
样例输出
18
练习3:4977_怪盗基德的滑翔翼
描述:怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。
有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。不得已,怪盗基德只能操作受损的滑翔翼逃脱。
假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。初始时,怪盗基德可以在任何一幢建筑的顶端。他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?
输入
输入数据第一行是一个整数K(K < 100),代表有K组测试数据。
每组测试数据包含两行:第一行是一个整数N(N < 100),代表有N幢建筑。
第二行包含N个不同的整数,每一个对应一幢建筑的高度h(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
练习4:友好城市
【问题描述】 Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。
【输入格式】
第1行,一个整数N(1<=N<=5000),表示城市数。
第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。(0<=xi<=10000)
【输出格式】
仅一行,输出一个整数,政府所能批准的最多申请数。
【输入样例】
7
22 4
2 6
10 3
15 12
9 8
17 17
4 2
【输出样例】
4
练习5:1058_合唱队形 2004年NOIP全国联赛提高组
描述:N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK, 则他们的身高满足T1<...<Ti>Ti+1>…>TK(1<=i<=K)。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入描述 InputDescription
输入文件chorus.in的第一行是一个整数N(2<=N<=100),表示同学的总数。第一行有n个整数,用空格分隔,
第i个整数Ti(130<=Ti<=230)是第i位同学的身高(厘米)。
输出描述 OutputDescription
输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
样例输入 SampleInput
8
186 186 150 200 160 130 197 220
样例输出 SampleOutput
4
练习6:1996_登山
描述:五一到了,PKU-ACM队组织大家去登山观光,队员们发现山上一个有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?
输入
Line 1: N (2 <= N <= 1000) 景点数
Line 2: N个整数,每个景点的海拔
输出
最多能浏览的景点数
样例输入
8
186 186 150 200 160 130 197 220
样例输出
4
练习7:5294_挖地雷
题目描述:在一个地图上有N个地窖(N<=20),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从第一个地窖开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。
设计一个挖地雷的方案,使某人能挖到最多的地雷。
输入描述 InputDescription
第1行只有一个数字,表示地窖的个数N。
第2行有N个数,分别表示每个地窖中的地雷个数。
第3行至第N+1行表示地窖之间的连接情况:
第3行有n-1个数(0或1),表示第一个地窖至第2个、第3个、…、第n个地窖有否路径连接。
如第3行为1 1 0 0 0 … 0,则表示第1个地窖至第2个地窖有路径,至第3个地窖有路径,至第4个地窖、第5个、…、第n个地窖没有路径。
第4行有n-2个数,表示第二个地窖至第3个、第4个、…、第n个地窖有否路径连接。
… …
第n+1行有1个数,表示第n-1个地窖至第n个地窖有否路径连接。(为0表示没有路径,为1表示有路径)。
输出描述 OutputDescription
第一行表示挖得最多地雷时的挖地雷的顺序,各地窖序号间以一个空格分隔,不得有多余的空格。
第二行只有一个数,表示能挖到的最多地雷数。
样例输入 SampleInput
5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1
样例输出 SampleOutput
1 3 4 5
27