posts - 195, comments - 34, trackbacks - 0, articles - 1

求数组中最长递增子序列

Posted on 2009-10-25 11:27 小强摩羯座 阅读(2886) 评论(1)  编辑  收藏 所属分类: 算法编程
最长递增子序列的求法 LIS (转)

什么是最长递增子序列呢?
问题描述如下:
   设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
对于这个问题有以下几种解决思路:
   1、把a1,a2,...,an排序,假设得到a'1,a'2,...,a'n,然后求a的a'的最长公共子串,这样总的时间复杂度为o(nlg(n))+o(n^2)=o(n^2);
   2、动态规划的思路:
    另设一辅助数组b,定义b[n]表示以a[n]结尾的最长递增子序列的长度,则状态转移方程如下:b[k]=max(max(b[j]|a[j]<a[k],j<k)+1,1);
    这个状态转移方程解释如下:在a[k]前面找到满足a[j]<a[k]的最大b[j],然后把a[k]接在它的后面,可得到a[k]的最长递增子序列的长度,或者a[k]前面没有比它小的a[j],那么这时a[k]自成一序列,长度为1.最后整个数列的最长递增子序列即为max(b[k]   | 0<=k<=n-1);
    实现代码如下:
    

#include <iostream>

using namespace std;

int main()

{

       int i,j,n,a[100],b[100],max;

       while(cin>>n)

       {

              for(i=0;i<n;i++)

                     cin>>a[i];

              b[0]=1;//初始化,以a[0]结尾的最长递增子序列长度为1

              for(i=1;i<n;i++)

              {

                     b[i]=1;//b[i]最小值为1

                     for(j=0;j<i;j++)

                            if(a[i]>a[j]&&b[j]+1>b[i])

                                   b[i]=b[j]+1;

              }

              for(max=i=0;i<n;i++)//求出整个数列的最长递增子序列的长度

                     if(b[i]>max)

                            max=b[i];

              cout<<max<<endl;

       }

       return 0;

}

    显然,这种方法的时间复杂度仍为o(n^2);
   3、对第二种思路的改进:
    第二种思路在状态转移时的复杂度为o(n),即在找a[k]前面满足a[j]<a[k]的最大b[j]时采用的是顺序查找的方法,复杂度为o(n).
    设想如果能把顺序查找改为折半查找,则状态转移时的复杂度为o(lg(n)),这个问题的总的复杂度就可以降到nlg(n).
    另定义一数组c,c中元素满足c[b[k]]=a[k],解释一下,即当递增子序列的长度为b[k]时子序列的末尾元素为c[b[k]]=a[k].
    先给出这种思路的代码,然后再对其做出解释。
    

#include <iostream>

using namespace std;

int find(int *a,int len,int n)//若返回值为x,a[x]>=n>a[x-1]

{

       int left=0,right=len,mid=(left+right)/2;

       while(left<=right)

       {

              if(n>a[mid]) left=mid+1;

              else if(n<a[mid]) right=mid-1;

              else return mid;

              mid=(left+right)/2;

       }

       return left;

}

void fill(int *a,int n)

{

       for(int i=0;i<=n;i++)

              a[i]=1000;

}

int main()

{

       int max,i,j,n,a[100],b[100],c[100];

       while(cin>>n)

       {

              fill(c,n+1);

              for(i=0;i<n;i++)

                     cin>>a[i];

              c[0]=-1;//    …………………………………………1

              c[1]=a[0];//        ……………………………………2

              b[0]=1;//     …………………………………………3

              for(i=1;i<n;i++)//        ………………………………4

              {

                     j=find(c,n+1,a[i]);//   ……………………5

                     c[j]=a[i];// ………………………………6

                     b[i]=j;//……………………………………7

              }

              for(max=i=0;i<n;i++)//………………………………8

                     if(b[i]>max)

                            max=b[i];

              cout<<max<<endl;

       }

       return 0;

}

    对于这段程序,我们可以用算法导论上的loop invariants来帮助理解.
    loop invariant: 1、每次循环结束后c都是单调递增的。(这一性质决定了可以用二分查找)
                           2、每次循环后,c[i]总是保存长度为i的递增子序列的最末的元素,若长度为i的递增子序

                                  列有多个,刚保存末尾元素最小的那个.(这一性质决定是第3条性质成立的前提)
                           3、每次循环完后,b[i]总是保存以a[i]结尾的最长递增子序列。
    initialization:    1、进入循环之前,c[0]=-1,c[1]=a[0],c的其他元素均为1000,c是单调递增的;
                           2、进入循环之前,c[1]=a[0],保存了长度为1时的递增序列的最末的元素,且此时长度为1

                                 的递增了序列只有一个,c[1]也是最小的;
                           3、进入循环之前,b[0]=1,此时以a[0]结尾的最长递增子序列的长度为1.
    maintenance:   1、若在第n次循环之前c是单调递增的,则第n次循环时,c的值只在第6行发生变化,而由

                                c进入循环前单调递增及find函数的性质可知(见find的注释),

                                 此时c[j+1]>c[j]>=a[i]>c[j-1],所以把c[j]的值更新为a[i]后,c[j+1]>c[j]>c[j-1]的性质仍然成

                                立,即c仍然是单调递增的;
                           2、循环中,c的值只在第6行发生变化,由c[j]>=a[i]可知,c[j]更新为a[i]后,c[j]的值只会变

                                  小不会变大,因为进入循环前c[j]的值是最小的,则循环中把c[j]更新为更小的a[i],当

                                 然此时c[j]的值仍是最小的;
                           3、循环中,b[i]的值在第7行发生了变化,因为有loop invariant的性质2,find函数返回值

                                为j有:c[j-1]<a[i]<=c[j],这说明c[j-1]是小于a[i]的,且以c[j-1]结尾的递增子序列有最大的

                               长度,即为j-1,把a[i]接在c[j-1]后可得到以a[i]结尾的最长递增子序列,长度为(j-1)+1=j;
    termination:       循环完后,i=n-1,b[0],b[1],...,b[n-1]的值均已求出,即以a[0],a[1],...,a[n-1]结尾的最长递

                              增子序列的长度均已求出,再通过第8行的循环,即求出了整个数组的最长递增子序列。

          仔细分析上面的代码可以发现,每次循环结束后,假设已经求出c[1],c[2],c[3],...,c[len]的值,则此时最长递增子序列的长度为len,因此可以把上面的代码更加简化,即可以不需要数组b来辅助存储,第8行的循环也可以省略。
    

#include <iostream>

using namespace std;

int find(int *a,int len,int n)//修改后的二分查找,若返回值为x,则a[x]>=n

{

       int left=0,right=len,mid=(left+right)/2;

       while(left<=right)

       {

              if(n>a[mid]) left=mid+1;

              else if(n<a[mid]) right=mid-1;

              else return mid;

              mid=(left+right)/2;

       }

       return left;

}

int main()

{

       int n,a[100],b[100],c[100],i,j,len;//新开一变量len,用来储存每次循环结束后c中已经求出值的元素的最大下标

       while(cin>>n)

       {

              for(i=0;i<n;i++)

                     cin>>a[i];

              b[0]=1;

              c[0]=-1;

              c[1]=a[0];

              len=1;//此时只有c[1]求出来,最长递增子序列的长度为1.

              for(i=1;i<n;i++)

              {

                     j=find(c,len,a[i]);

                     c[j]=a[i];

                     if(j>len)//要更新len,另外补充一点:由二分查找可知j只可能比len1

                            len=j;//更新len

              }

              cout<<len<<endl;

       }

       return 0;

}

最长递增部分序列 Longest Ordered Subsequence Extention hoj10027 poj2533
2007-08-21 20:19

求最长递增部分序列是一个比较常见的动态规划题。在导弹拦截等题中都有用到。一般来说就是用经典的O(n^2)的动态规划算法。

算法如下:

         设A[i]表示序列中的第i个数,F[i]表示从1到i这一段中以i结尾的最长上升子序列的长度,初始时设F[i] = 0(i = 1, 2, ..., len(A))。则有动态规划方程:F[i] = max{1, F[j] + 1} (j = 1, 2, ..., i - 1, 且A[j] < A[i])。

然而在hoj10027中n的值达到了50000。显而易见经典算法是会超时滴。所以只有另谋出路了。

         用一个变量len记录到目前为止所找出来的最长递增序列的长度。另外准备一个数组b[],用这个数组表示长度为j的递增序列中最后一个元素的值。在这里长度为j的递增序列不止一个,我们所要保存是那个最小的。为什么呢?因为最后一个元素越小,那么这个递增序列在往后被延长的机会越大。初始化b[0] = -1;len = 0;从第一个元素a[1]开始       a[i]( 1 <= i <= n)。如果这个元素比len长的序列的最大值大。则把这个元素直接添加到b数组的后面。如果这个元素比b数组的第一个元素还要小则把这个元素赋给b数组的第一个值。否则进行二分查找。当在b数组里面找到一个数比a[i]小,并且他的后面的数大于或等于a[i]则跳出。将a[i]添加到这个数的后面。输出len就可以了。

代码如下:

#include <stdio.h>
#include <string.h>
int main()
{
int a[50001], b[50001];
int i, j, l, r, len, n, mid;
while (scanf("%d", &n) != EOF)
{
   for (i = 0; i < n; i++)
    scanf("%d", &a[i]);
   len = 0;
   memset(b, 0, sizeof(int) * 50001);
   b[0] = -1;
   for (i = 0; i < n; i++)
   {
    if (a[i] > b[len])
     b[++len] = a[i];
    else if (a[i] < b[1] )
     b[1] = a[i];
    else
    {
     l = 1; r = len;
     while (l <= r)
     {
      mid = (l + r)>>1;
      if (a[i] > b[mid] && a[i] <= b[mid + 1])
      {
       j = mid;
       break;
      }
      if (b[mid] > a[i])
      {
       r = mid - 1;
      }
      else
      {
       j = mid;
       l = mid + 1;
      }
     }
     b[j + 1] = a[i];
    }
   }
   printf("%d\n", len);
}
return 0;



Feedback

# re: 求数组中最长递增子序列  回复  更多评论   

2012-02-09 11:58 by 琉璃囧
这是原创麽?但是如果要输出LIS的元素..2 3 7 6 8 4 5 9 1的输出结果不正确吖~怎么改进才可以得到正确的序列呢?

只有注册用户登录后才能发表评论。


网站导航: