最长递增子序列
【实验目的】
1 掌握动态规划和LCS的相关算法
2 利用动态规划的思想实现最长递增子序列
3 分析实验结果,总结算法的时间和空间复杂度。思考是否能将算法的时间复杂度提高到O(nlgn)
【系统环境】
Windows XP 平台
【实验工具】
VC++6.0英文企业版
【实验原理】(若涉及算法设计)
描述: 随机生成小于等于n的自然数的一个序列,输出其最长递增子序列(任意一个即可)。
n 分别取 1000,3000,10000。
例: n=5 随机序列为 5 1 4 2 3,正确输出为1 2 3,即长度为3的递增子序列。
分析:
1使用动态规划的前提条件:要求一个序列的最大递增子序列最优解问题可以转化为求其子问题的最优解问题,而且其子问题包含重叠的子问题比如此题1 2 3这个序列包含1 2这个序列,我们可以通过查找计算。(注意子序列可能不唯一)
2 仿照LCS问题,a存放随机生成长度为n的数组元素。构造一个数组f,它的每一个值存放以a [i]元素为结尾的最长子序列的长度,则此问题的递归关系式如下:
f[i]= 1 if i=0
else
(当满足a[j]<a[i]取出这些f[j]并查找最大值,如果没有max f[j]为0)
3解释:在a[i]前面找到满足a[j]<a[i]的最大f[j],然后把a[i]接在它的后面(用pre记录前一个数的下标),可得到a[i]的最长递增子序列的长度,或者a[i]前面没有比它小的a[j],那么这时a[i]自成一序列(它的pre[i]=-1),长度为1.最后整个数列的最长递增子序列即为max(f[j]| 0<=j<=n-1);
4 C语言源码设计如下:
void lis_length(long *array,long n)
{
long i,j,max=0;
long *f = checkedMalloc(n*sizeof(*f)); //生成数组f,用于存放每个递增子序列的长度值;
long *pre = checkedMalloc(n*sizeof(*pre));//生成数组pre,保存前一个递增数的下标,便于打印递增序列
*(f+0) = 1; //以第array[0]为末元素的最长递增子序列长度为1,即f[0]=1;
(*pre) = -1; //第一个数的前一个小于它的数下标记作-1
for(i = 1;i<n;i++)
{
*(f+i)=1;//f[i]的最小值为1;
*(pre+i) = -1;//每次赋初值为-1
for(j=0;j<i;j++)//循环i 次 ***************
{
if(array[j]<array[i]&&*(f+j)+1>*(f+i))
{
*(f+i) = *(f+j)+1;//更新f的值。
*(pre+i) = j; //更新前一个递增数的下标
}
}
}
for(i=0;i<n;i++) //查找子序列长度最大的一个(可能不唯一,每次都返回末尾元素最大的一个)
{
if(*(f+i)>max)
{
max = *(f+i);
j = i; //保留当前下标
}
}
printf("最长递增子序列的长度为:%ld/n",max);
lis_print(array,pre,j);
printf("/n");
free(f);
free(pre);
return ;
}
很显然此问题的时间复杂度由两层for循环决定,为O( )(其他都为O(n),不影响渐进时间),在空间复杂度方面,它需要两个长度为n的额外数组来保存相关数据(一个为f,保存每个递增子序列长度;一个为pre保存每个数前一个递增数的下标).
问题提出:
我们能否将时间复杂度提高到O(nlgn)呢?答案是,完全可以。
分析:决定这个程序时间复杂度的关键在于标注“*”号的for循环处,由于第一种方法采用顺序查找满足条件的f[i]值,故时间复杂度为O( ),如果我们能使用折半查找的话,那么时间复杂度就能调高到
O(nlgn)了。但是折半查找是需要关键字有序的,所以我们还需要额外的一个数组c用来维护这个有序的数组,完成折半查找。c中元素满足c[f[i]]=a[i],解释一下,即当递增子序列的长度为f[i]时子序列的末尾元素为c[f[i]]=a[i].为了保存相关元素的下标,我们还需要一个数组p用来记录在数组c中的每个元素在原数组a中的标,便于pre更新相关下标,具体设计见代码。
void modifiedlis_length(long *array,long n)
{
long i,max=0,left,right,mid;
long len; //保存最大子序列长度
long *pre = checkedMalloc(n*sizeof(*pre));//生成数组pre,保存前一个递增数的下标,便于打印递增序列
long *c = checkedMalloc(n*sizeof(*c));//按照单调递增记录每次子序列末尾元素
long *p = checkedMalloc(n*sizeof(*p));//记录在array数组中的原始下标,以便更新pre域
(*pre) = -1;
*(p+0) = -1;
*(c+0) = -1;//0位置元素不用的好处为len长度正好对应最大下标
*(c+1) = array[0];
*(p+1) = 0; //初始化第一个元素和其下标
len = 1; //初始的递增子序列长度为1,因为只含有一个元素
for(i = 1;i<n;i++)
{
*(pre+i) = -1;//每次赋初值为-1
left=0l;right=len;mid=(left+right)/2;
while(left<=right)//循环i次
{
if(array[i]>c[mid]) left=mid+1;
else right=mid-1;
mid=(left+right)/2;
}
c[left]=array[i];
p[left]=i;
pre[i]=p[left-1];
if(left>len)
{
len = left;//更新最大子序列长度
}
}
printf("最长递增子序列的长度为:%ld/n",len);
//printf("p[len]=%ld/n",p[len]);
lis_print(array,pre,p[len]);
printf("/n");
free(c);
free(p);
return ;
}
【实验结果】
n=10时实验结果显然正确。
n=1000时,结果也是正确的,而且发现改进后的时间小于改进前的时间。
这是n=3000的运行结果
这是n=10000的运行结果,时间差别已经很明显了
【实验总结】
本实验用动态规划的相关思想解决了求最长递增子序列问题,前一种方法时间复杂度为O(n*n),后一方法时间复杂度为O(nlgn),但是后一种方法要比前一种方法在空间复杂度上多一个数组,所以这就是我们常说的用空间换时间。
有些图片显示不出来,需要完整版请到这里下载http://download.csdn.net/source/818592