动态规划专题之最长上升子序列

专题四:最长上升子序列

/*

       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

猜你喜欢

转载自blog.csdn.net/QiaoRuoZhuo/article/details/80721238