动态规划

 

  ****知识点****

  最长递增或递减子系列(Longest Increasing/Decreasing  Series,LIS/LDS)是指一个序列中按照单调性可构成最长单调的的序列。

  最长公共子序列(Longest Common Series,LCS)是指两个序列对象中共同包含的相同部分构成的子序列,可依据题意考虑单调性或不考虑。

  最长子串(Longset Substring,LS)是指给定串中任意个连续的字符组成的子序列。

 

   ****入门题****

  第一题:求一个字符串的最长递增子序列长度,如azbcdef的最长递增子序列为abcdef,长度为6.

  输入:

  第一行是一个整数N(0<N<20),表示有N个字符串需要处理;之后的N行为一个字符串,串长度不超过1000.

  输出:

  输出字符串的最长递增子序列的长度。

 

  解题分析:

  有序列{a1,a2,a3,...,an},按照递推求解的思想,用F[i]代表递增子序列以ai结束时的最长长度。当i为1时即以a1结束时最长长度为1,则有F[1]=1.在除了长度为1

的情况以外,当i<x时如果满足ai<ax,则ax可以跟在以ai结尾的递增子序列之后形成新的递增子序列,综合两种情况,可以得到最长递增子序列的长度为:

(1)F[1];(2)F[1]+F(x),等价于F[x]=max{1,1+F[i]|ai<ax & i<x} (动态规划经典)

  注释部分为解法1,非注释部分为解法2.

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 #define maxn 10001
 4 
 5 char s[maxn];
 6 int dp[maxn];
 7 int max_num;
 8 
 9 /*
10 void LIS()
11 //最长子序列函数 
12 {
13     int len;
14     memset(dp,0,sizeof(dp));
15     //初始化dp数组为0 
16     len = strlen(s);
17     for(int i = 0; i < len; i++)
18     {
19         dp[i] = 1;
20         //未出现单调递增情况时序列长度均初始化为1 
21         for(int j = 0; j < i; j++)
22         //每满足单调递增条件:i>j,ai>aj 
23         //同时满足条件:i>j,dp[i]<dp[j]+1 
24         //对序列长度进行F[i]=1+F[j] 
25         {
26             if(s[i] > s[j] && dp[i] < dp[j] + 1)
27                 dp[i] = 1 + dp[j];
28         } 
29     }
30     max_num = 0;
31     //初始化最长序列长度为0
32     //在下面找到dp数组最大值输出 
33     for(int i = 0;i < len; i++)
34         if(max_num < dp[i])
35             max_num = dp[i]; 
36 }
37 
38 
39 int main(void)
40 {
41     int n;
42     while(scanf("%d",&n) != EOF)
43     {
44         while(n--)
45         {
46             scanf("%s",s);
47             //输入每组字符串 
48             LIS();
49             //调用最长序列长度求解函数 
50             printf("%d\n",max_num);
51             //输出 
52         }
53     }
54     return 0;
55  } 
56  */
57  
58  //以下方法主要是借鉴学习. 
59  int main(void)
60  {
61      int n;
62      int max_num = 1;
63      int dp[maxn];
64      dp[0] = -999;
65      //初始化dp数组首元素,此处dp数组为存储数组. 
66      scanf("%d",&n);
67      while(n--)
68      {
69          scanf("%s",s);
70          //输入字符串 
71          for(int i = 0; i < strlen(s); i++)
72          {
73          //比较每个输入字符串的字符 
74              for(int j = max_num - 1; j >= 0; j--)
75              {
76          //只有在j=maxnum-1时满足if条件才能继续下一个字符
77         //否则需要替换dp中上一个存储的字符来继续下一个 
78                  if((int)s[i] > dp[j])
79                  {
80                     dp[j+1] = s[i];
81         //dp用于存储最长子序列中字符 
82                     if(j + 1 == max_num)
83                         max_num++;
84                     break;
85                 }
86             }
87         }
88         printf("%d\n",max_num-1);
89         //输出最长递增序列长度 
90     }
91     return 0;
92  }

 

  第二题:POJ2945 拦截导弹

  描述:

  某国为了防御敌国的导弹袭击,开发出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于

前一发的高度。某天,雷达捕捉到敌国的导弹来袭,并观测到导弹依次飞来的高度,请计算这套系统最多能拦截多少导弹。拦截来袭导弹时,必须按来袭导弹袭击的时间顺序,不

许先拦截后面的导弹,再拦截前面的导弹。

  输入:

  输入有两行,
  第一行,输入雷达捕捉到的敌国导弹的数量k(k<=25),
  第二行,输入k个正整数,表示k枚导弹的高度,按来袭导弹的袭击时间顺序给出,以空格分隔。
  输出:

  输出只有一行,包含一个整数,表示最多能拦截多少枚导弹。

  样例输入:

 

  8
  300 207 155 300 299 170 158 65
 样例输出:
  6

 

  解题分析

  设初始导弹的距离为d[i],之后每一发导弹的距离为d[j],当i>j时,根据题意要求每一发导弹高度都不高于前一发的高度即“≤”关系,转换为等式关系:i>j时,d[i]≤d[j].与最长递增子序列问题相比,本问题可以抽象为最长准递减子序列(准为前后两个距离可以相等的含义)。针对导弹的距离序列为{a1,a2,a3,...,an},用F[i]代表导弹

距离序列以ai命中目标时的最高距离点。当i为1时即以a1结束时最高距离点为1,则有F[1]=1.在除了距离点数为1的情况以外,当i<x时如果满足ai>ax,则ax可以认为是以ai距离

处同一发导弹降落的距离点,综合两种情况,可以得到导弹最长准递减子序列的长度为:

(1)F[1];(2)F[1]+F(x),等价于F[x]=max{1,1+F[i]|ai≥ax & i<x} 

  在拦截导弹问题的动态规划AC代码基础上将LIS函数中的单调递增条件:i>j,ai>aj 修改为i>j,ai≤aj即可。具体AC代码如下:

 1 //本质求单调最长递减子序列 
 2 #include <bits/stdc++.h>
 3 using namespace std;
 4 
 5 const int maxn = 25;
 6 int a[maxn],dp[maxn],m,Max;
 7 
 8 void LDS()
 9 {
10     memset(dp,0,sizeof(dp));
11     for(int i = 0;i < m;i++)
12     {
13         dp[i] = 1;
14         //满足条件i>j,ai<aj && i>j,dp[i]<dp[j]+1
15         for(int j = 0;j < i;j++)
16             if(a[i] <= a[j] )
17                 dp[i] = max(dp[i],dp[j]+1);    
18     }
19     
20     Max = 0;
21     //寻找dp数组中最长序列数 
22     for(int i = 0;i < m;i++)
23         if(Max < dp[i])
24             Max = dp[i];
25 }
26 int main(void)
27 {
28     scanf("%d",&m);
29     for(int i = 0;i < m; i++)
30         scanf("%d",&a[i]);
31     LDS();
32     printf("%d\n",Max);
33 
34     return 0;
35 } 

 

  以下两种动态规划方程表述方式本质上是相同的。

 1          //满足条件i>j,ai<aj && i>j,dp[i]<dp[j]+1
 2          for(int j = 0;j < i;j++)
 3             if(a[i] <= a[j] )
 4               dp[i] = max(dp[i],dp[j]+1);  
 5 
 6 
 7 
 8         //满足条件i>j,ai<aj && i>j,dp[i]<dp[j]+1
 9         for(int j = 0;j < i;j++)
10             if(a[i] <= a[j] && dp[i] < dp[j] + 1)
11                 dp[i] = dp[j] + 1;                 

  另一个可参考的成功AC代码:*max_lelment(a,a+len)表示输出集合最大元素,*min_element表示输出集合最小元素.

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int maxn=25;
 5 int dp[maxn],a[maxn];
 6 
 7 int main()
 8 {
 9     int n;
10     cin>>n;
11     for(int i=1;i<=n;i++) 
12     { 
13         cin>>a[i]; 
14         dp[i]=1;
15     }
16 
17     for(int i=1;i<=n;i++)
18         for(int j=1;j<i;j++)
19         {
20             if(a[i]<=a[j]) 
21                 dp[i]=max(dp[i],dp[j]+1);
22         }
23 
24     cout<<*max_element(dp+1,dp+n+1); 
25     return 0;
26  
27 }    

 

  第三题:POJ1458 最长公共子序列

  描述:

  A subsequence of a given sequence is the given sequence with some elements (possible none) left out. Given a sequence X = < x1, x2, ..., xm > another sequence Z = < z1, z2, ..., zk > is a

subsequence of X if there exists a strictly increasing sequence < i1, i2, ..., ik > of indices of X such that for all j = 1,2,...,k, xij = zj. For example, Z = < a, b, f, c > is a subsequence of X = < a, b, c, f,

b, c > with index sequence < 1, 2, 4, 6 >. Given two sequences X and Y the problem is to find the length of the maximum-length common subsequence of X and Y.

  输入:

  The program input is from the std input. Each data set in the input contains two strings representing the given sequences. The sequences are separated by any number of white spaces. The

input data are correct.

  输出:

  For each set of data the program prints on the standard output the length of the maximum-length common subsequence from the beginning of a separate line.

  样例输入: 

 abcfbc         abfcab
 programming contest
 abcd mnp
 样例输出:
 4
 2
 0 

 解题分析:
 字符数组s1,s2,分别从编号1开始即不存在第0个字符,如果s1i为字符数组s1的前i个字符构成的子串,s2j为字符数组s2的前j个字符构成的子串,用dp[i][j]表示s1,s2的
公共子串
最大长度。参考”https://blog.csdn.net/hrn1216/article/details/51534607”这篇博客的理论推导,主要考虑三种情况:
 (1)i=0或j=0时最长公共子序列长度为0
 (2)s1[i]=s2[j]时dp数组的主对角线元素满足递推:dp[i][j]=dp[i-1][j-1]
 (3)s1[i]!=s2[j]时dp数组的副对角线元素满足递推:dp[i][j]=max(dp[i][j-1],dp[i-1[][j])
 可用下图来表示该过程:

          

  *求解最长公共子序列时可以采用逆推法,但需要注意的是s1[i]!=s[j]即dp[1][j-1]=dp[i-1][j]存在分支时逆推的方向总体上要相同,要么向上要么向左,如果出现向上向左

都有则可能会导致恢复的子序列错误。针对上述分析给出以下关于求最长公共子序列长度的AC代码:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define maxn 1000
 5 char str1[maxn],str2[maxn];
 6 //定义两个序列 
 7 int dp[maxn][maxn];
 8 //两序列的最大公共长度 
 9 
10 int main(void)
11 {
12     while(scanf("%s%s",str1+1,str2+1) > 0)
13     //输入两个序列并增加一个字节“吃”回车 
14     {
15         int length1 = strlen(str1+1);
16         int length2 = strlen(str2+1);
17         //获取两个序列的长度 
18         for(int i = 0; i < length1; i++)
19             dp[i][0] = 0;
20         for(int j = 0 ;j < length2; j++)
21             dp[0][j] = 0;
22         //对i=0或j=0的dp数字初始化为0 
23         for(int i = 1; i <= length1; i++)
24             for(int j = 1; j<= length2 ; j++)
25                 if(str1[i] == str2[j])
26                     dp[i][j] = dp[i-1][j-1] + 1;
27         //当遍历到两字符相同时对公共长度加1 
28                 else
29                 {
30                     int m = dp[i][j-1];
31                     int n = dp[i-1][j];
32                     if(m > n)
33                         dp[i][j] = m;
34                     else
35                         dp[i][j] = n;
36         //元素不相同时判断副对角线上公共长度 
37                 }
38         printf("%d\n",dp[length1][length2]);
39     }
40     return 0;
41 }

 

  第四题:POJ2711 合唱队形

  描述:

  N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学不交换位置就能排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1

, 2, …, K,他们的身高分别为T1, T2, …, TK,则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1 <= i <= K)。你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。(Ti为中间点)

  输入:

  输入的第一行是一个整数N(2 <= N <= 100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位同学的身高(厘米)。

  输出:

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

  样例输入

 8
 186 186 150 200 160 130 197 220
 样例输出
 4

 

  解题分析

   本题由于需要求解最少需要几个同学出列,这可以理解为求正向最大递增子序列和最大递减子序列这两个序列的最大长度,递减子序列的求解可以从后往前推即将最后一个同学认为是1。最少需要出列的人数可以采用总人数减去上述两个序列数的和后并加1的办法来输出(加1是因为中间断点计算了两次)。

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define maxn 200
 5 
 6 int main(void)
 7 {
 8     int n; //同学人数
 9     int a[maxn];//同学身高
10     int dp1[maxn],dp2[maxn];//记录最长递增、递减子序列长度 
11     scanf("%d",&n);
12     for(int i = 0; i < n; i++)
13         scanf("%d",&a[i]);//输入身高
14     
15     memset(dp1,0,sizeof(dp1));//初始化dp1 
16     memset(dp2,0,sizeof(dp2));//初始化dp2 
17     
18     //最长递增子序列 
19     for(int i = 0; i < n; i++ )
20     {
21         dp1[i] = 1;
22         for(int j = 0; j < i; j++)
23             if(a[i] > a[j])
24                 dp1[i] = max(dp1[i],dp1[j]+1);
25     }
26     
27     //最长递减子序列 
28     for(int i = n - 1; i >= 0; i--)
29     {
30         dp2[i] = 1;
31         for(int j = n - 1; j > i; j--)
32         {
33             if(a[i] > a[j])
34                 dp2[i] = max(dp2[i],dp2[j]+1);
35         }
36     }
37     
38     //求解最大值 
39     int temp = 0;
40     for(int i = 0; i < n; i++)
41         if(dp1[i] + dp2[i] - 1 > temp)
42             temp = dp1[i] + dp2[i] - 1;
43     
44     //输出出列同学人数        
45     printf("%d\n",n-temp);
46     
47     return 0;
48 }

 

  第五题:POJ4131 魅力手链

  描述:

  Bessie has gone to the mall's jewelry store and spies a charm bracelet. Of course, she'd like to fill it with the best charms possible from the N(1 ≤ N≤ 3,402) available charms. Each charm iin the supplied list has a weight Wi(1 ≤ Wi≤ 400), a 'desirability' factor Di(1 ≤ Di≤ 100), and can be used at most once. Bessie can only support a charm bracelet whose weight is no more than M(1 ≤ M≤ 12,880).

Given that weight limit as a constraint and a list of the charms with their weights and desirability rating, deduce the maximum possible sum of ratings.

  输入:

  Line 1: Two space-separated integers: N and M
  Lines 2..N+1: Line i+1 describes charm i with two space-separated integers: Wi and Di输出Line 1: A single integer that is the greatest sum of charm desirabilities that can be achieved given the weight constraints

  输出:

  Line 1: A single integer that is the greatest sum of charm desirabilities that can be achieved given the weight constraints.

  样例输入:

  4 6
  1 4
  2 6
  3 12
  2 7

  样例输出:

  23

 

  解题分析:

  首先对样例进行解释,样例输入第一行中4为四种项链,6为最大可承受的重量,第二列第二行至第五行中值为项链价值属性。

  该问题等价于01背包问题,即项链重量有限情况下选择尽可能多的项链使得项链总价值最大化。

  如果采用枚举法,每条项链有放入背包或不放入背包两种可能,那么复杂度为O(2n),显示不可取。

  如果从递推角度考虑,对物品的重量和价值编号为W[i],D[i]后,优先考虑第N种物品,看处理后剩下的问题是否和原问题相同并且规模可减小。

  如果从动态规划角度考虑,可以研究dp[i][j],在前i种物品中取若干种,总体积不超过j的条件下能获得的最大价值。

  如果取了第i种物品,那么则要在前i-1种物品中选取若干种,在其总体积不超过j-W[i]的条件下所能获得的最大价值即F(i-1,j-W[i]);

  如果不取第i种物品,那么问题变成了F(i-1,j).总结来说,问题等价于当i>1时的F(i,j)=max(F(i-1,j),F(i-1,j-W[i])),而当i=1时有F(i,j)=D[1](W[1] <= j)或0(W[1] > j). 

   

  ***以下为根据上述思路编写的程序***

  这份代码在测试时遇到一个小插曲:主要是由于自己的粗心,下面代码的dp[Weight_Num]处被输成了dp[Max_Num],这导致了数组空间开小,于是在2021年1月的某个时间POJ这道题的评测系统出现了本人的RE疯狂刷屏,不过终于发现了这个粗心的地方。RE出现的问题有(除以0,数组越界,指针越界,使用已经释放的空间,数组开得太大超出栈范围)。

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define Max_Num 3405
 5 #define Weight_Num 12885
 6 
 7 int main(void)
 8 {
 9     int n,m;
10     int W[Max_Num],D[Max_Num],dp[Weight_Num];
11     while(scanf("%d%d",&n,&m) != EOF)
12     {
13         for(int i = 0 ; i < n; i++)
14         {
15             scanf("%d%d",&W[i],&D[i]);
16             dp[i] = 0;
17         } 
18             
19         //当j>=W[i]时的动规条件
20         for(int i = 0; i < n; i++)
21         {
22             for(int j = m; j >= W[i]; j--)
23                 dp[j] = max(dp[j - W[i]] + D[i],dp[j]);
24         }
25         printf("%d\n",dp[m]);
26     }
27     return 0;
28 }

 

  第六题:POJ1088 滑雪

   描述:

  Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:

 1  2  3  4 5
 16 17 18 19 6
 15 24 25 20 7
 14 23 22 21 8
 13 12 11 10 9

  一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-...-3-2-1更长。事实上,这是最长的一条。

  输入:

  输入的第一行表示区域的行数R和列数C(1 <= R,C <= 100)。下面是R行,每行有C个整数,代表高度h,0<=h<=10000。

  输出:

  输出最长区域的长度。

  样例输入:

 5 5
 1 2 3 4 5
 16 17 18 19 6
 15 24 25 20 7
 14 23 22 21 8
 13 12 11 10 9

  样例输出

 25

    

  对上述样例分析:红色路径表示仅能走一条,绿色路径表示能走两条,黄色路径表示能走三条,紫色路径表示能走四条,高度优先为紫色>黄色>绿色>红色。

  L(2,2)=max(L(2,1),L(3,2),L(2,3),L(1,2))+1

  L(2,1)=max(L(1,1),L(2,0))+1

  L(1,2)=max(L(1,1),L(0,2),L(1,3))+1

  L(2,3)=max(L(3,3),L(3,4))+1

  L(3,2)=max(L(3,1),L(4,2))+1

  .......(以此递推)

  L(0,0)=1 

 

 解题分析:
 如果用L(i,j)表示从点(i,j)出发的最长滑行长度,(i,j)周围没有更低的点则L(i,j)=1,(i,j)周围有更低的点则L(i,j)等于(i,j)周围四个点高度比(i,j)低
且L值最大的点的值加1。如上述样例的解析过程即可体现分析的思想,但是上述样例能输出的前提是由L(0,0)形成了闭环。实际测试中是无法确定L(0,0)是否为1的,这就要求
找到输入的所有点数中高度最小的点即需要进行排序处理。在进行排序处理前应当将存储点集合的二维数组展开为一维数组后进行排序,同时一维数组又保留原来二维数组对应的行
列下标属性值,以便直接在一维数组基础上进行上述的比较处理。排序完成后,对每个点的L值初始化为1,开始按从低到高计算所有点的L。
 根据以上分析可以得到以下AC代码:
 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define points_num 11000
 5 #define size_num 110
 6 struct Point
 7 {
 8     int r,c;
 9     int h;
10     bool operator <(const Point & p)const
11     {
12         return h < p.h;//从小到大
13     }
14 }points[points_num];
15 //采用一维数组存储便于后续的排序寻找最小高度点 
16 
17 int a[size_num][size_num];
18 //输入的点矩阵 
19 int dp[size_num][size_num];
20 //关于点(i,j)的最长滑行长度
21 int row,col;
22  
23 int main(void)
24 {
25     scanf("%d%d",&row,&col);
26     
27     for(int i = 0; i < row; i++)
28         for(int j = 0; j < col; j++)
29         { 
30             scanf("%d",&a[i][j]);
31             points[i * col + j].h = a[i][j];
32             points[i * col + j].r = i;
33             points[i * col + j].c = j;
34             dp[i][j] = 1;
35         } 
36     //一维数组展开处理
37         
38     sort(points,points + row * col);
39     //对points点集合排序
40      
41     for(int i = 1; i < row * col; ++i)//从1开始是因为排序后第0个元素是最小的,这个元素的最长长度即为1
42     {
43         int r = points[i].r;
44         int c = points[i].c;
45         if(r > 0 && a[r-1][c] < a[r][c])
46             dp[r][c] = max(dp[r-1][c] + 1,dp[r][c]);
47         if(r < row - 1 && a[r+1][c] < a[r][c])
48             dp[r][c] = max(dp[r+1][c] + 1,dp[r][c]);
49         if(c > 0 && a[r][c-1] < a[r][c])
50             dp[r][c] = max(dp[r][c-1] + 1,dp[r][c]);
51         if(c < col - 1 && a[r][c+1] < a[r][c])
52             dp[r][c] = max(dp[r][c+1] + 1,dp[r][c]);
53     }
54     //以上则是判断上下左右方向是否有路可走 
55     
56     int temp = 0;
57     for(int i = 0; i < row; i++)
58         for(int j = 0; j < col; j++)
59             temp = max(temp,dp[i][j]);
60     printf("%d\n",temp);
61     //输出最大长度 
62     
63     return 0;
64 }

  上述分析和代码是已知最低点的最长长度通过递推来求解最高点的最长长度,是用若干个已知状态的值推出一个未知状态的值。

 


 
posted @ 2021-01-19 22:01  小黑鱼吃码粮  阅读(10)  评论(0编辑  收藏