DP·最长递增子序列

问题描述

设序列L={a1,a2,,an}n个不同的整数序列,序列L中最长递增子序列LIS={ak1,ak2,,akm},其中k1<k2<<kmak1<ak2<<akm。求最大的m值。

思路分析

1、把a1,a2,...,an排序,假设得到a'1,a'2,...,a'n,然后求aa'的最长公共子串,这样总的时间复杂度为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);(见状态转移方程)

状态转移方程

设辅助数组b[],定义b[n]表示以a[n]结尾的最长递增子序列的长度,状态转移方程表示为:

b[k] = max ( max(b[j]|a[j]<a[k], j<k)+1, 1 )

其中0<=k<=n-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),此时时间复杂度仍为O(n^2)

另外定义一数组cc中元素满足c[b[k]]=a[k],即当递增子序列的长度为b[k]时子序列的末尾元素为c[b[k]]=a[k]

实现代码 

第一种思路:

#include<stdio.h>  //时间复杂度O(n^2)

int main()

{

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

       while(scanf("%d",&n)!=EOF)

       {

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

                     scanf("%d",&a[i]);

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

              c[0]=a[0]; k=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[i]+1;

                            }

              }

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

                     if(b[i]>max) 

                            max=b[i];

                     printf("%d/n",max);

       }

       return 0;

}

第二种思路:

#include<stdio.h>  //时间复杂度(O(nlogn)),存在缺陷,对c的赋值,简化则不存在

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(scanf("%d",&n)!=EOF)

       {

              fill(c,n+1);  //!!!!!!!!!!!!!!!!重要的地方

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

                     scanf("%d",&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];

                     printf("LIS的长度为:%d/n",max);

                     printf("具体的数据为:/n");

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

                            printf("%d ",c[i]);

                     printf("/n");

       }

       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的性质2find函数返回值为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<stdio.h>

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],c[100],i,j,len;  //新开一变量len,用来储存每次循环结束后c中已经求出值的元素的最大下标

       while(scanf("%d",&n)!=EOF)

       {

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

                     scanf("%d",&a[i]);

              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

              }

              printf("LIS的长

度为:%d/n",len);

              printf("具体元素为:/n");

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

                     printf("%d ",c[i]);

              printf("/n");

       }

       return 0;

      

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值