前言
ok,这两天又研究了关于最长递增子序列相关的一些内容。记录在此,希望便人便己
问题描述
求一个序列的最长递增子序列,这样的子序列是允许中间越过一些字符的,即留“空”。
例如:序列A: 4 2 3 1 5 的最长递增子序列为 2 3 5,长度为 3 。
解法一
分析
这题其实和上次那个最大连续子序列和有点像,解法一的思路也和最大连续子序列和问题有些许联系。可以采用类似的解空间的思路。
对于给定的一个求解序列来说,假定其最终的答案是b0,b1,b2…bj(这里的b0,b1,b2…都是从原序列中抽取的递增值,其中bj是最后一个元素,也是最大的一个),那么可以肯定bj要么是a0,要么是a1,…要么是a(n-1).也就是说,最终求解的序列的最大值,要么是以a0为结尾的序列,要么是a1结尾的序列…
举个栗子,还是题目中的那个序列:最终的答案可以是以下几种:
(1)、以4为结尾的序列,只有一种:就是4
(2)、以2为结尾的序列,一种:2 (注意(4 2)不是递增的)
(3)、以3为结尾的序列,两种:3 或者(2 3)
…
所以我们可以确定一个条件,然后所有的条件都组合起来。还是一句话,这里并不是说要用暴力解法,全部都列举出来,只是用这种思路(先确定解空间的一个条件)来构造符合动态规划的性质和特点,也就是重复子问题和最优子结构。
经过上述的分析,我们可以用lcs[i] 表示 以第i个结点为递增序列的最后一个结点的所有序列中最长的序列长度。
说的有点拗口哦,以题目所给的序列作为例子,比如说lcs[2]就是以3为结尾的递增序列中长度最长的那个,以3位结尾的递增序列有两种3 和 2 3 ,最长的长度肯定就是2了。所以lcs[2] = 2.
构造了lcs[ ]之后,我们可以得到动态规划的很重要的一个属性,就是最优子结构性质。具体一点就是说,lcs[i] 的求解可以从lcs[j],(0<=j <= i-1) 得到,说的在透明些,也就是说,lcs[i]的求解不需要重复i元素之前的所有序列情况,只要用到前面记录的所有lcs[j] 就可以(0<=j <= i-1) ),这也是动态规划思想的一个精髓,用记表的方式避免了重复计算相同的单元
最终我们可以得到这样一个递推式:
好了递推式出来了,代码就不成问题了。
代码
//利用动态规划求解最长递增子序列
int LongIncSeq(int data[],int n)
{
int lis[n] = {0};
int longest = 0;
for(int i = 0;i<n;++i)
{
int tmpLon = 0; //记录以i为结束结点的,最长递增子序列
for(int j = i-1;j>=0;--j) //判断data[0]-data[i-1]所有比data[i]小的元素
{
if(data[i] > data[j])
{
if(lis[j] >tmpLon) //取较大的值
tmpLon = lis[j];
}
}
lis[i] = tmpLon+1; //记录以i为尾部的最长的递增子序列
if(lis[i]>longest) //记录整体最长的
longest = lis[i];
}
return longest;
}
注:以上算法的复杂度很容易看出来是O(N2)级别的
解法二
分析
解法二比较巧妙,照我看来,还是从某种程度上用了解空间的这种思维方式。它是这样分解解空间的:
假设递增子序列B是一个解,那么B长度可能的取值是
0,1,2,3,…, n-1. 这个挺容易理解的。
那么我可以在遍历元素的过程中,记录着长度为i的递增子序列最大元素的最小值。用maxElem[i]表示。
具体来说:
(1)、长度为1的递增子序列最大元素的最小值为maxElem[1]
(2)、长度为2的递增子序列最大元素的最小值为maxElem[2]
(3)、长度为2的递增子序列最大元素的最小值为maxElem[3]
… … …
比如说对于序列A = {1,-1,2,-3,4,-5,6,-7}
当i为4时,
长度为1的递增序列有:1,-1,2,-3 这四个,其中的最小值为-1
长度为2的递增序列有:(1,2),(-1,2),其中的最小值为2
长度为3的递增序列没有。
与此同时,还要记录一个遍历到当前位置时,产生的最大的递增子序列,也就是随时更新maxElem[]数组的大小,假设用变量longIncSeq表示。
在遍历的时候主要的操作是用当前元素A[i] 和maxElem[j](j>=0 && j<=longIncSeq ),进行比较 (当然是从后往前比较)。
如果A[i]大于某个maxElem[j],就说明可以形成一个以A[i]为最大元素的子序列。如果这个新形成的子序列长度大于longIncSeq,就更新longIncSeq的值。同时要更新对应长度子序列最大元素(也就是末尾元素)的最小值。
我知道,看起来写的有些复杂,真正动动你的小手指,模拟一下就基本清楚了。
代码
//另一种动态规划的解法:最长递增子序列
int LongIncseq(int data[],int n)
{
int longIncSeq = 0; //记录最长的递增子序列的长度
int maxElem[n] = {0}; //记录长度为i的递增子序列最大元素(也就是序列的末尾元素)的最小值
maxElem[0] = (*min_element(data,data+n))-1; //最小元素减1
for(int i = 0;i<n;++i)
{
int j;
for( j= longIncSeq;j>=0;--j) //寻找所有序列长度在longIncSeq—0之间
{
if(data[i] > maxElem[j]) //data[i]大于序列长度为j的最大元素
{
break;
}
}
if(j == longIncSeq)
{
++longIncSeq;
maxElem[longIncSeq] = data[i]; //最新的序列长度
}
else if(maxElem[j]<data[i] && maxElem[j+1] > data[i]) //新长度为j+1的序列,其最大值小于原来的那个就更换
{
maxElem[j+1] = data[i];
}
}
return longIncSeq;
}
总结
稍稍总结一下吧,两点内容:
(1)、动态规划算法针对的是问题的求解过程,而不是针对问题本身。我们在有了一个思路之后,可以按照这个思路,探究是否可以使用动态规划的思想,就我来看就是记表、以空间换时间。
(2)、分解解空间,先假设确定一个条件,或许可以作为上面的一种探究思路。