最长递增子序列
令S是不同整数x1,x2,….,xn的序列。S的递增序列(IS)是序列xi1,xi2,….xik,其中i1<i2<….ik,使得对任意的1<=j<k,有xij<xij+1。S的最长递增序列(LIS)是具有最大长度的递增序列。
问题:给定一个由不同整数组成的序列,求它的最长递增序列。
归纳假设(首次尝试):给定某个长度小于m的序列,知道如何求它的某个最长的递增序列。
归纳基础包含长度为1的序列,结论显然。给定长度为m的序列,我们先对前m-1个元素求得其中的一个LIS,然后考察xm。如果xm比LIS中最后一个归纳的元素大,则xm就能加到LIS的末尾,形成新的LIS,解答完毕。如果xm不大于LIS中最后一个归纳的元素,则问题无法继续解决。例如可能有多个LIS,而xm也许只能使其中一个更长,而这个LIS未必就是由归纳法得到的那个。
导致上述归纳失败的原因是,可能存在多个最长的LIS。再进一步归纳:
归纳假设(第二次尝试):给定某个长度小于m的序列,知道如何求它的所有最长的递增序列。
这个归纳假设也不行,假如xm不能使任何LIS变长,还有可能存在比最长的短1的IS,且xm可以使它变长,从而得到新的LIS。而我们如果想求第二长的IS,就必须求出所有第三长和第四长的IS,如此等等,这充分说明了这个归纳假设过强了。
那是否真的需要所有的LIS吗?我们只想知道xm是否能让其中某个LIS变长,那么能否找到其中最有可能变长的LIS呢?在所有的LIS中,末尾最小的那个数是最有希望的。
归纳假设(第三次尝试):给定某个长度小于m的序列,知道如何求它的某个最长的递增序列LIS,使得其他LIS的末尾的数都比这个LIS末尾的数大。
这个归纳假设也存在于第二次归纳时同样的情形。 现在问题已经很明朗了,不能随意丢弃短的IS,也许其中某个IS就是最终那个LIS的起始序列。
我们用BIS(k)表示长度为k的最可能的递增序列,即以最小数结尾的序列(如果不止一个,则任取其中某个)。把BIS(k)中末尾的数记为BIS(k).last
归纳假设(第四次尝试):给定某个长度小于m的序列,知道如何对任意k<m-1求出BIS(k),如果存在的话。
归纳基础显然成立。给定xm,要求出它可以改变的那些BIS。xm能加长某个BIS(k)当且仅当下列两个条件成立:(1)xm>BIS(k),于是xm可加入BIS(k);(2)xm<BIS(k+1).last,于是BIS(k)末尾加上xm后比BIS(k+1)的可能性更大。算法如下:给定某个xm,依次查看BIS(i).last的值,其中i=s,s-1,s-2等,知道找到某个BIS(j).last小于xm。如果这样的j不存在,那么xm就是当前序列中最小的数,并将其作为BIS(1)。若j=s,则把xm加在BIS(s)后,构造出一个新的BIS(s+1)。否则,有BIS(j).last<xm<BIS(j+1).last,于是用BIS(j+1)替换BIS(j)xm。这个算法还可以进一步优化,由于集合是有序的,所以可以用二叉搜索进行查找。这样,每个xm最多增加O(logm)次比较,整个运行时间为O(nlog n)。现在问题变为,如何设计数据结构来实现上述算法。
参考代码:
int binarysch(const int *arr, int len,int key)
{
if(arr == NULL || len < 0)
return -1;
int left = 0;
int middle = 0;
int right = len - 1;
//为什么要判断=呢?因为可能left、right的值在循环前不等,循环后相等呢,此时还应该处理这个值
while(left <= right)
{
middle = left + (right - left) / 2;
if(arr[middle] < key)
left = middle + 1;
else if(arr[middle] > key)
right = middle - 1;
}
return right;
}
int LIS(int *arr,int length)
{
if(arr == NULL || length <=0)
return -1;
//初始化为0
int *lenarr = new int(length);
memset(lenarr,0,length);
//第一个数,此时长度为1,以它自己结尾
lenarr[0] = arr[0];
//记录LIS
int curlen = 1;
for(int i = 1; i < length; ++i)
{
//直接添到末尾,得到新的LIS
if(arr[i] > lenarr[curlen-1])
lenarr[++curlen - 1]=arr[i];
else
{
//查找可能变长的IS
int pst = binarysch(lenarr,curlen,arr[i]);
lenarr[pst+1] = arr[i];
}
}
return curlen;
}
int main(int argc, char **argv)
{
int arr[] = {7,2,3,1,2,4,5,8,9,10,11,6,8,9,10,11};
int lis = LIS(arr,16);
for(;;);
return(0);
}
经过上述分析,可知,当第一次进行归纳时,未必能得到结果,此时需要调整归纳假设;当我们对第i步进行分析时,可能还有很多沉余的信息。例如,假设知道如何求长度小于m的所有IS,现在来考虑m+1,此时需要记录所有已m+1为结尾的IS。那么怎么记录这些信息呢,该用什么样的数据结构呢?之前我就被困在这里,不知道如何记录所有的IS,所以想到算法,还应当能实现出来。最后想到用二维数组来记录,i表示IS长度,j表示当前处理的长度,则[i,1…j]表示构成所有长度为i的末尾的数为j。对这个算法在仔细分析,并不需要记录所有的IS,这就是改进,性能好的算法并不是一步想出来的。