参考:http://www.ahathinking.com/archives/117.html(一个非常牛的人)
http://www.felix021.com/blog/read.php?1587(牛人)
来自互联网,自己做了修改
一个各公司都喜欢拿来做面试笔试题的经典动态规划问题,互联网上也有很多文章对该问题进行讨论,但是我觉得对该问题的最关键的地方,这些讨论似乎都解释的不很清楚,让人心中不快,所以自己想彻底的搞一搞这个问题,希望能够将这个问题的细节之处都能够说清楚。
对于动态规划问题,往往存在递推解决方法,这个问题也不例外。要求长度为i的序列的Ai{a1,a2,……,ai}最长递增子序列,需要先求出序列Ai-1{a1,a2,……,ai-1}中以各元素(a1,a2,……,ai-1)作为最大元素的最长递增序列,然后把所有这些递增序列与ai比较,如果某个长度为m序列的末尾元素aj(j<i)比ai要小,则将元素ai加入这个递增子序列,得到一个新的长度为m+1的新序列,否则其长度不变,将处理后的所有i个序列的长度进行比较,其中最长的序列就是所求的最长递增子序列。举例说明,对于序列A{35, 36, 39, 3, 15, 27, 6, 42}当处理到第九个元素(27)时,以35, 36, 39, 3, 15, 27, 6为最末元素的最长递增序列分别为35
35,36
35,36,39
3
3,15
3,15,27
3,6
当新加入第10个元素42时,这些序列变为
35,42
35,36,42
35,36,39,42,
3,42
3,15,42
3,15,27,42
3,6,42
这其中最长的递增序列为(35,36,39,42)和(3,15,27,42),所以序列A的最长递增子序列的长度为4,同时在A中长度为4的递增子序列不止一个。
该算法的思想十分简单,如果要得出Ai序列的最长递增子序列,就需要计算出Ai-1的所有元素作为最大元素的最长递增序列,依次递推Ai-2,Ai-3,……,将此过程倒过来,即可得到递推算法,依次推出A1,A2,……,直到推出Ai为止,
按照上面思路做出的代码:
这个方法的时间复杂度为O(n^2)。这是一个基本的算法。
//最长的递增子序列
#include<iostream>
using namespace std;
int LIS(int* a,int *len,int size)
{
if(a==NULL||size<=0)
return -1;
int maxlen=0;
for(int i=0;i<size;i++)
{
len[i]=1;
for(int j=0;j<i;j++)
{
if(a[j]<a[i]&&len[i]<len[j]+1)
{
len[i]=len[j]+1;
if(len[i]>maxlen)
maxlen=len[i];
}
}
}
return maxlen;
}
///打印出最长的递增子序列
void Display_Lis(int *a,int index,int maxlen,int* len)
{
bool IsLis=false;
if(index<0||maxlen<=0)
return;
if(len[index]==maxlen)//maxlen不断在递减,每一次当len[index]=maxlen对应的index就是要取的一个值
{
--maxlen;
IsLis=true;
}
Display_Lis(a,--index,maxlen,len);//因为是从后往前推进的所以要倒序的打印
if(IsLis)
cout<<a[index+1]<<" ";//注意这里输出一定标号是index+1,因为递归那里是--index
}
int main()
{
int a[] = {1,-1,2,-3,4,-5,6,-7};
int size=sizeof(a)/sizeof(int);
int *len=new int[size];//记录下最长的子序列的长度
int result=LIS(a,len,size);
cout<<result<<endl;
Display_Lis(a,size-1, result, len);
cout<<endl;
return 0;
}
方法二:排序+LCS
这个方法是在Felix’blog(见参考资料)中看到的,因为简单,他在博文中只是提了一句,不过为了练手,虽然懒,还是硬着头皮写一遍吧,正好再写一遍快排,用quicksort + LCS,这个思路还是很巧妙的,因为LIS是单调递增的性质,所以任意一个LIS一定跟排序后的序列有LCS,并且就是LIS本身。代码如下:
//求最长公共递增子序列
//这里是结合了求最长公共序列的方法+快速排序
//新建一个数组是原来数组排序后的结果,求新数组和原来数组的最长公共子序列
//因为新数组是排序后的所以这个最长公共子序列一定就是最长的递增子序列
#include<iostream>
using namespace std;
void swap(int * arr, int i, int j)
{
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
void qsort(int * arr, int left, int right)
{
if(left >= right)
return ;
int index = left;//参考点
for(int i = left+1; i <= right; ++i)
{
if(arr[i] < arr[left])
{
swap(arr,++index,i);
}
}
swap(arr,index,left);
qsort(arr,left,index-1);
qsort(arr,index+1,right);
}
//求最长公共子序列
int Lcs(int *a,int **len,int *a_copy,int size)
{
for(int i=1;i<=size;i++)//注意这里i,j从1开始取,所以size就一定要取到
for(int j=1;j<=size;j++)
{
if(a[i-1]==a_copy[j-1])//因为i,j从1开始取,所以这里要减去1
len[i][j]=len[i-1][j-1]+1;
else if(len[i-1][j]<len[i][j-1])
len[i][j]=len[i][j-1];
else
len[i][j]=len[i-1][j];
}
return len[size][size];
}
void Display_Lcs(int *a,int *a_copy,int **len,int maxlen,int size)
{
int i=size,j=size;
int *result=new int[maxlen];
int index=maxlen;
while(i&&j)
{
if(a[i-1]==a_copy[j-1])
{
result[--index]=a[i-1];//注意这里不能够写为--maxlen,否则在输出result时用maxlen就会出错
i--;
j--;
}
else if(a[i-1]!=a_copy[j-1]&&len[i][j-1]>len[i-1][j])
{
j--;
}
else
{
i--;
}
}
for(int k=0;k<maxlen;k++)
cout<<result[k]<<" ";
cout<<endl;
}
int main()
{
int a[] = {1,-1,2,-3,4,-5,6,-7};
int size=sizeof(a)/sizeof(int);
int **len=new int*[size+1];//注意这里len大小至少都是size+1,因为为了避免讨论边界情况,外围是一圈0
for(int i=0;i<size+1;i++)
len[i]=new int[size+1];
for(int i=0;i<size;i++)
for(int j=0;j<size;j++)
len[i][j]=0;
int *a_copy=new int[size];
memcpy(a_copy,a,sizeof(a));//这里必须写为sizeof(a)
for(int i=0;i<size;i++)
cout<<a_copy[i]<<" ";
cout<<endl;
qsort(a_copy,0,size-1);
int maxlen=Lcs(a,len,a_copy,size);
cout<<maxlen<<endl;
Display_Lcs(a,a_copy,len,maxlen,size);
return 0;
}
memcpy函数的用法:http://blog.csdn.net/lovemysea/article/details/5275612
http://www.cppblog.com/kang/archive/2009/04/05/78984.html
http://www.cnblogs.com/stoneJin/archive/2011/09/16/2179248.html
方法3 O(nlogn)复杂度的做法
最长递增子序列,Longest Increasing Subsequence 下面我们简记为 LIS。
排序+LCS算法 以及 DP算法就忽略了,这两个太容易理解了。
假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1
然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1
接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2
再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3
第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了
第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。
于是我们知道了LIS的长度为5。
!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。
然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!
参考:http://www.ahathinking.com/archives/117.html
http://blog.csdn.net/joylnwang/article/details/6766317
程序参考的是编程之美
另外需要注意的是这里的而二分搜索,不要弄错了,弄清楚这里要求的二分搜索返回的index具体的含义。
#include<iostream>
using namespace std;
int BinarySearch(int a[],int length,int n)
{
int start=0,end=length-1;
while(start<=end)
{
int middle=start+(end-start)/2;
if(a[middle]<n)
start=middle+1;
else
end=middle-1;
}
return end;//这个end满足MaxV[end]<n的第一个,即刚好满足的小于n,MaxV是递增的的一个有序的数组
}
int Min(int a[],int length)
{
int min=a[0];
for(int i=1;i<length;i++)
{
if(a[i]<min)
min=a[i];
}
return min;
}
//LIS[i]存储的是以array[i]为最大元素的最长递增子序列的长度为LIS[i]
//关键LIS[i]中存储了array[i]的位置信息i
//MaxV[k]存储的是长度为k的递增子序列最大元素的最小值
int LIS(int a[],int length)
{
int *MaxV=new int[length+1];//因为如果i<j,那么就会有MaxV[i]<MaxV[j],所以MaxV就是一个递增的数组,可以用二分搜索来加快搜索
MaxV[1]=a[0];//数组中第一值
MaxV[0]=Min(a,length)-1;//数组中比最小值还小,作为边界值,这个很有用,保证了二分搜索时一定存在a[i]大于MaxV[]中的某数,
//这个一定要有,可以简化后面判断
int *LIS=new int[length];
for(int i=0;i<length;i++)//注意这个LIS不是最长递增序列,它只是存储的对应长度LIS的最小末尾
LIS[i]=1;//这里初始化为1的原因就是对于每个a[i],其作为最大元素的最长递增子序列的长度至少为1
int MaxLis=1;//数组最长递增子序列的长度
for(int i=1;i<length;i++)
{
int index=BinarySearch(MaxV,MaxLis+1,a[i]);//注意是在MaxV数组中搜索,而MaxV数组有值的长度为MaxLis+1
LIS[i]=index+1;//这里得到了以a[i]为最大元素的最长递增子序列的长度为index+1
if(LIS[i]>MaxLis)//如果当前最长序列大于最长递增序列长度跟新MaxLis、MaxV
{
MaxLis=LIS[i];//以array[i]为最大元素的最长递增子序列的长度为LIS[i]
MaxV[LIS[i]]=a[i];//长度为LIS[i]的递增子序列最大元素的最小值是a[i];
}
else if(MaxV[index]<a[i]&&a[i]<MaxV[index+1])//当前最长序列不大于最长递增序列长度,index是刚好MaxV[index]小于a[i],
//如果还满足a[i]<MaxV[index+1],那就更新MaxV[index+1]的值
{
MaxV[index+1]=a[i];//这里LIS[i]=index+1
}
}
/*****************打印输出**************************/
int *result=new int[MaxLis];
int MaxLis_index=BinarySearch(LIS,length,MaxLis);//注意这里二分搜索返回的是刚好小于MaxLis的
MaxLis_index++;//加1后才是MaxLis对应的a[i]的坐标i
int len=MaxLis;
result[--len]=a[MaxLis_index];
for(int i=MaxLis_index;i>=0;i--)
{
int temp=BinarySearch(LIS,i,len);
result[--len]=a[++temp];
if(len==0)
break;
}
for(int j=0;j<MaxLis;j++)
cout<<result[j]<<" ";
cout<<endl;
/*****************************************************/
return MaxLis;
}
int main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
int length=sizeof(arr)/sizeof(int);
int result=LIS(arr,length);
cout<<result<<endl;
return 0;
}
如果不要求输出递增子数组,实际并不需要用LIS[i]数组来记录标号,直接用个len就可以了,最后直接输出MaxV数组的长度就是最长递增子数组的长度了。
//求最长递增公共子序列,这种方法虽然简便但是只能很快的求出最长递增子序列长度无法输出序列
#include<iostream>
using namespace std;
int BinarySearch(int a[],int length,int n)
{
int start=0,end=length-1;
while(end-start>1)
{
int middle=start+(end-start)/2;
if(a[middle]<n)
start=middle;
else
end=middle;
}
return end;//返回a数组中刚刚大于等于n的那个元素的下标,这个下标对应的数是要替换掉的
}
int LIS(int a[],int length,int *b)
{
int len=1;//len就是最终求到的最长递增子序列的长度
for(int i=0;i<length;i++)
{
if(a[i]>b[len-1])//b数组不是LIS,它只是存储的对应长度LIS的最小末尾
b[len++]=a[i];
else
{
int pos=BinarySearch(b,length,a[i]);
b[pos]=a[i];
}
}
return len-1;//注意这里要len-1,因为上面len++
}
int main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
int length=sizeof(arr)/sizeof(int);
int *b=new int[length];
int result=LIS(arr,length,b);
cout<<result<<endl;
return 0;
}