【专题】—【DP】—【最长增长子序列】

参考:http://qiemengdao.iteye.com/blog/1660229

不一定增长,减小也一样的→_→


一、复杂度为 O(n^2)  的方法:

【主要用来初步理解,题目容易超时】


给一个初始序列: a[10] = { 2 , 5 , 6 , 1 , 3 , 7  , 5 , 9 , 8 , 1 0 } ,【下标从0开始】,求它的的最长增长子序列的长度,并输出该序列

先给出答案吧:max_len=6 , 序列:{ 2 , 5 , 6 , 7 , 9 , 10 }


需要三个数组:

a[10]    ——   存放最初的数组

f[10]     ——   存放以a[i]为结尾的序列的长度,初始化全为1 

       【因为如果a[i]前面没有比a[i]小的,那么a[i]就是子序列的开头和结尾,长度为1】

pos[10]    ——    存放a[i]所在的序列中,a[i]的前一个的位置,因为最后需要输出子序列,要用到它的。。。如果不需要输出,则不需要该数组

      【初始化为自己的下标就好了。。。】














然后定义 i , j ,遍历一遍a[i],对每个a[i] ,去前面找 a[i] 能接在哪个序列后面

i=0 ,前面没有,不动 【i完全可以从1开始】


i=1 ,a[1]=5,去前面找

          j=0 ,a[1]>a[0] ,f[1]<f[0]+1 【这说明a[i]接到a[j]后面,能让序列变长,如果f[1]>f[0]+1,接后面吃亏,      如果相等,接后面白费力气】,说明a[1] 可以接在 a[0] 后面,组成序列 {2,5} ,

          然后更新f[1]=2 【长度为2】 ,pos[1]=0【因为5上一个元素是2,下标是0】


i=2 ,a[2]=6,去前面找


          j=0  ,   a[2]>a[0] ,f[2]<f[0]+1,说明a[2] 可以接在 a[0] 后面,组成序列 {2,6} ,

                     然后更新f[2]=2 【长度为2】 ,pos[2]=0【因为6上一个元素是2,下标是0】


          j=1  ,a[2]>a[1] ,f[2]<f[1]+1 ,说明a[2] 可以接在 a[1] 后面,组成序列 {2,5,6} ,

                     然后更新f[2]=3 【长度为3】 ,pos[2]=1【因为6上一个元素是5,下标是1】


i=3 ,  a[3]=1,去前面找

         j=0   ,a[3]<a[0] ,不能接在后面

j=1   ,a[3]<a[1] ,不能接在后面

         j=2   ,a[3]<a[2] ,不能接在后面

                     然后f[3],pos[3]都不用更新了。。。


i=4 ,a[4]=3,去前面找

          j=0   ,   a[4]>a[0] ,f[4]<f[0]+1,说明a[4] 可以接在 a[0] 后面,组成序列 {2,3} ,

                      然后更新f[4]=2 【长度为2】 ,pos[4]=0【因为3上一个元素是2,下标是0】


          j=1   ,a[4]<a[1] ,不能接在后面

          j=2   ,a[4]<a[2] ,不能接在后面


                 j=3   ,a[4]>a[3] ,但是(f[4]=2) = (f[3]+1) ,接在后面不会让最长序列的长度增长,所以不需要更新~~~

……后面的类似→_→当然程序中需要定义max_len 不断和f[i]比较,更新,记录子串的最大长度


最终:











最后是输出最大长度max_len

还要输出该子串,曾经做过一道题,用了递归输出,结果超时。。。o(╯□╰)o

后来是用了一个数组,把下标全部存进去了。。。再输出。。。AC


上代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
using namespace std;

int a[1000];    //存放初始序列的数组
int f[1000];    //存放以a[i]结尾的序列的最大长度
int pos[1000];  //存放a[i]所在序列的前一项的位置
int temp[1000]; //最后输出子序列的时候,用来存放下标,是倒序的
int max_len;    //存放子序列的最长长度,每次更新
int max_subscript;   //最长序列结尾元素的下标

int main()
{
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        max_len=max_subscript=0;  //初始化为0
        for(int i=0;i<n;i++)    //初始化
        {
            scanf("%d",a+i);
            f[i]=1;     //f[]数组初始化为1,因为若a[i]前面没有比它小的,那么以a[i]结尾的子串长度为1
            pos[i]=i;   //pos[]数组初始化为自己的下标
        }

        for(int i=0;i<n;i++)    //a[i]数组遍历一遍,对每个a[i],去前面找能将a[i]最为结尾的序列
        {
            for(int j=0;j<i;j++)    //对每个a[i],去前面找能将a[i]最为结尾的序列
            {
                if(a[i]>a[j])   //最长增长子序列,当然得后一个比前一个大咯~~~如果是递减,小于即可
                    if(f[i]<f[j]+1) //这个限制条件一开始有点难相同,挺难说明的,看着数组,多推演几遍就能想明白的→_→
                    {
                        f[i]=f[j]+1;
                        pos[i]=j;   //记录前驱的下标,后面会用于输出
                        if(f[i]>max_len)
                        {
                            max_len=f[i];   //更新最长长度
                            max_subscript=i;    //更新最长序列结尾的下标
                        }
                    }
            }
        }
        printf("%d\n",max_len); //输出最长的子序列的长度

//        for(int i=0;i<n;i++)    //输出f[]数组看看
//            printf("%d ",f[i]);
//        printf("\n");
//
//        for(int i=0;i<n;i++)    //输出pos[]数组看看
//            printf("%d ",pos[i]);
//        printf("\n");

        for(int i=0;i<max_len;i++)  //记录下标
        {
            temp[i]=max_subscript;
            max_subscript=pos[max_subscript];   //往前推
        }

        for(int i=max_len-1;i>=0;i--)     //输出最长增长子序列
            printf("%d ",a[temp[i]]);
        printf("\n");
    }
    return 0;
}



二、复杂度为 O(nlogn)  的方法:


这个方法我暂时不会输出子序列。。。

给一个初始序列: a[10] = { 2 , 5 , 6 , 1 , 3 , 7  , 5 , 9 , 8 , 1 0 } ,【下标从0开始】,求它的的最长增长子序列的长度

先给出答案吧:max_len=6 , 序列:{ 2 , 5 , 6 , 7 , 9 , 10 }


需要两个数组:

a[10]    ——   存放最初的数组

f[10]     ——   存放中间结果,并不是子序列,不需要初始化成什么,每次之前的值会被覆盖的

int len  ——  记录长度,初始化为0


先说为什么复杂度是O(nlogn)

首先O(n) 是遍历一遍 a[] 数组,如果 a[i] 比f[]数组中最大的数【就是最后那个】还要大,直接放入 f[] 数组最后,长度+1

但是如果 a[i] 不比最大的大,对每个a[i],去 f[] 数组中查找能插入的位置,就是找到第一个大于等于它的数的位置,覆盖掉,但是不会影响长度,所以len不变。

该过程采用二分的方法找位置,所以复杂度是O(logn)

最坏情况就是每个a[i]都要二分去找插入的位置,所以最坏情况复杂度就是O(nlogn)



len=0

第一步:f[0]=a[0] ,len=1

然后开始遍历,i从1开始

i=1:a[1]>f[0],放入f[]数组,f[1]=a[1]=5 ,len=2,f[]:{2,5}

i=2:a[2]>f[1],放入f[]数组,f[2]=a[2]=6 ,len=3,f[]:{2,5,6}

i=3:a[3]<f[2],在f[]中找第一个大于等于a[3]的数,即f[0],用a[3]覆盖,长度不变,len=3,f[]:{1,5,6}

i=4:a[4]<f[2],在f[]中找第一个大于等于a[4]的数,即f[1],用a[4]覆盖,长度不变,len=3,f[]:{1,3,6}

i=5:a[5]>f[2],放入f[]数组,len=4,f[]:{1,3,6,7}

i=6:a[6]<f[3],在f[]中找第一个大于等于a[6]的数,即f[2],用a[6]覆盖,长度不变,len=4,f[]:{1,3,5,7}

i=7:a[7]>f[3],放入f[]数组,f[4]=a[7]=9 ,len=5,f[]:{1,3,,5,7,9}

i=8:a[8]<f[4],在f[]中找第一个大于等于a[8]的数,即f[4],用a[8]覆盖,长度不变,len=5,f[]:{1,3,5,7,8}

i=9:a[9]>f[4],放入f[]数组,f[5]=a[9]=10 ,len=6,f[]:{1,3,5,7,8,10}


f[]数组仅仅用来存放中间结果,

f[]中的序列不是最长递增子序列。。。

但是长度正是最长递增子序列的长度,


上代码:


#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
using namespace std;

int a[1000];    //存放初始数列
int f[1000];    //存放中间结果,不是最长增长子序列

int BiSearch (int x,int left,int right)
{
    if(left<=right)
    {
        int mid=(left+right)/2;
        if(f[mid]==x)
            return mid;
        if(f[mid]>x)
            return BiSearch(x,left,mid-1);
        if(f[mid]<x)
            return BiSearch(x,mid+1,right);
    }
    else
        return left;    //可以推演一下,如果f[]中不存在与a[i]相等的元素,那么第一个比它大的一定是最后left>right的left
}

int main()
{
    int n,len;
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=0;i<n;i++)
            scanf("%d",a+i);    //初始化a[]数组

        f[0]=a[0];      //把a[0]存入f[0],然后就开始遍历了。。。
        len=1;      //len表示最长增长子序列的长度,初始化为1,因为f[0]=a[0];了
        for(int i=1;i<n;i++)
        {
            if(a[i]>f[len-1])   //如果a[i]>f[]中最大的,就直接加进f[]数组,长度加1
                f[len++]=a[i];
            else                //否则,在f[]数组中找到第一个大于等于a[i]的数的下标,用a[i]覆盖,长度不变
            {
                int pos=BiSearch(a[i],0,len-1); //二分查找【递归】
                f[pos]=a[i];    //覆盖
            }
        }
        printf("%d\n",len);     //最后输出最长递增子序列的长度

//        for(int i=0;i<len;i++)    //打印f[]数组,里面存的是中间结果,不是最长递增子序列
//            printf("%d ",f[i]);
//        printf("\n");
    }
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值