• 最长上升序列 LCS LIS


    子序列问题

    (一)一个序列中的最长上升子序列(LISLIS)

    n2做法 直接dp即可:

     for(int i=1;i<=n;i++)
        {
            dp[i]=1;//初始化 
            for(int j=1;j<i;j++)//枚举i之前的每一个j 
            if(data[j]<data[i] && dp[i]<dp[j]+1)
            //用if判断是否可以拼凑成上升子序列,
            //并且判断当前状态是否优于之前枚举
            //过的所有状态,如果是,则↓ 
            dp[i]=dp[j]+1;//更新最优状态 
    
        }
    n2

    上述的方法 dp定义为以第i个数字为结尾的最长上升子序列长度 注意是以第i个数组为结尾 而不是前i个!!!!!!!!!! (我目前学的线性dp一般就是前i个 如背包  还有就是着重于第i个  这两种 虽然下面马上就是第三种了。。)

    下一状态最优值=最优比较函数(已经记录的最优值,可以由先前状态得出的最优值)

    ——即动态规划具有 判断性继承思想

    nlogn做法:

    我们其实不难看出,对于n^2n2做法而言,其实就是暴力枚举:将每个状态都分别比较一遍。但其实有些没有必要的状态的枚举,导致浪费许多时间,当元素个数到了10^4-10^5104105以上时,就已经超时了。而此时,我们可以通过另一种动态规划的方式来降低时间复杂度:

    将原来的dp数组的存储由数值换成该序列中,上升子序列长度为i的上升子序列,的最小末尾数值

    这其实就是一种几近贪心的思想:我们当前的上升子序列长度如果已经确定,那么如果这种长度的子序列的结尾元素越小,后面的元素就可以更方便地加入到这条我们臆测的、可作为结果、的上升子序列中。

    int n;
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>a[i];
            f[i]=0x7fffffff;
            //初始值要设为INF
            /*原因很简单,每遇到一个新的元素时,就跟已经记录的f数组当前所记录的最长
            上升子序列的末尾元素相比较:如果小于此元素,那么就不断向前找,直到找到
            一个刚好比它大的元素,替换;反之如果大于,么填到末尾元素的下一个q,INF
                    就是为了方便向后替换啊!*/ 
        }
        f[1]=a[1];
        int len=1;//通过记录f数组的有效位数,求得个数 
        /*因为上文中所提到我们有可能要不断向前寻找,
        所以可以采用二分查找的策略,这便是将时间复杂
        度降成nlogn级别的关键因素。*/ 
        for(int i=2;i<=n;i++)
        {
            int l=0,r=len,mid;
            if(a[i]>f[len])f[++len]=a[i];
            //如果刚好大于末尾,暂时向后顺次填充 
            else 
            {
            while(l<r)
            {   
                mid=(l+r)/2;
                if(f[mid]>a[i])r=mid;
        //如果仍然小于之前所记录的最小末尾,那么不断
        //向前寻找(因为是最长上升子序列,所以f数组必
        //然满足单调) 
                else l=mid+1; 
            }
            f[l]=min(a[i],f[l]);//更新最小末尾 
            }
        }
        cout<<len;
    View Code

     简化:

    #define MAXN 40005
     
    int arr[MAXN],ans[MAXN],len;
     
    int main()
    {
        int T,p,i,j,k;
        scanf("%d",&T);
        while(T--){
            scanf("%d",&p);
            for(i=1; i<=p; ++i)
                scanf("%d",&arr[i]);
            
            ans[1] = arr[1];
            len=1;
            for(i=2; i<=p; ++i){
                if(arr[i]>ans[len])
                    ans[++len]=arr[i];
                else{
                    int pos=lower_bound(ans,ans+len,arr[i])-ans;
                    ans[pos] = arr[i];
                   }
            }
        }
            printf("%d
    ",len);
        
        return 0;
    }
    View Code

    终极简化:

    #include<bits/stdc++.h>
    using namespace std;
    //input
    #define rep(i,a,b) for(int i=(a);i<=(b);i++)
    #define RI(n) scanf("%d",&(n))
    #define RII(n,m) scanf("%d%d",&n,&m);
    #define RIII(n,m,k) scanf("%d%d%d",&n,&m,&k)
    #define RS(s) scanf("%s",s);
    #define LL long long
    #define REP(i,N)  for(int i=0;i<(N);i++)
    #define CLR(A,v)  memset(A,v,sizeof A)
    //////////////////////////////////
    const int INF=0x3fffffff;
    int a[105], dp[105];
    int main (void)
    {
        int n, ans = 0; cin>>n;
        fill(dp, dp + n, INF);
        for (int i = 0; i < n; i++){
            cin>>a[i];
            *lower_bound(dp, dp + n, a[i]) = a[i];
        }
        cout<<lower_bound(dp, dp +n, INF) - dp<<endl;
        return 0;
    }
    View Code

     注意  上升序列用 low_bound 不下降序列用upper_bound

    但是事实上,nlognnlogn做法偷了个懒,没有记录以每一个元素结尾的最长上升子序列长度。那么我们对于n^2n2的统计方案数,有很好想的如下代码(再对第一次的dpdp数组dpdp一次):

    for(i = 1; i <= N; i ++){
        if(dp[i] == 1) f[i] = 1 ;
        for(j = 1; j <= N: j ++)
            if(base[i] > base[j] && dp[j] == dp[i] - 1) f[i] += f[j] ;
            else if(base[i] == base[j] && dp[j] == dp[i]) f[i] = 0 ;
        if(f[i] == ans) res ++ ;
        }
    View Code

    但是nlognnlogn呢?虽然好像也可以做,但是想的话会比较麻烦,在这里就暂时不讨论了qwq,但笔者说这件事的目的是为了再次论证一个观点:时间复杂度越高的算法越全能

    输出路径

    只要记录前驱,然后递归输出即可(也可以用栈的)

    下面贴出n ^ 2n2的完整代码qwq

    #include <iostream>
    using namespace std;
    const int MAXN = 1000 + 10;
    int n, data[MAXN];
    int dp[MAXN]; 
    int from[MAXN]; 
    void output(int x)
    {
        if(!x)return;
        output(from[x]);
        cout<<data[x]<<" ";
        //迭代输出 
    }
    int main()
    {
        cin>>n;
        for(int i=1;i<=n;i++)cin>>data[i];
    
        // DP
        for(int i=1;i<=n;i++)
        {
            dp[i]=1;
            from[i]=0;
            for(int j=1;j<i;j++)
            if(data[j]<data[i] && dp[i]<dp[j]+1)
            {
                dp[i]=dp[j]+1;
                from[i]=j;//逐个记录前驱 
            }
        }
    
        int ans=dp[1], pos=1;
        for(int i=1;i<=n;i++)
            if(ans<dp[i])
            {
                ans=dp[i];
                pos=i;//由于需要递归输出
        //所以要记录最长上升子序列的最后一
        //个元素,来不断回溯出路径来 
            }
        cout<<ans<<endl;
        output(pos);
    
        return 0;
    View Code

    (二)两个序列中的最长公共子序列(LCSLCS)

    1、譬如给定2个序列:

    1 2 3 4 5
    
    3 2 1 4 5

    试求出最长的公共子序列。

    qwqqwq显然长度是33,包含3 4 53  4  5 三个元素(不唯一)

    解析:我们可以用dp[i][j]dp[i][j]来表示第一个串的前ii位,第二个串的前j位的LCSLCS的长度,那么我们是很容易想到状态转移方程的:

    如果当前的A1[i]A1[i]和A2[j]A2[j]相同(即是有新的公共元素) 那么

    dp[ i ] [ j ] = max(dp[ i ] [ j ], dp[ i-1 ] [ j-1 ] + 1);dp[i][j]=max(dp[i][j],dp[i1][j1]+1);

    如果不相同,即无法更新公共元素,考虑继承:

    dp[ i ] [ j ] = max(dp[ i-1 ][ j ] , dp[ i ][ j-1 ]dp[i][j]=max(dp[i1][j],dp[i][j1]

    #include<iostream>
    using namespace std;
    int dp[1001][1001],a1[2001],a2[2001],n,m;
    int main()
    {
        //dp[i][j]表示两个串从头开始,直到第一个串的第i位 
        //和第二个串的第j位最多有多少个公共子元素 
        cin>>n>>m;
        for(int i=1;i<=n;i++)scanf("%d",&a1[i]);
        for(int i=1;i<=m;i++)scanf("%d",&a2[i]);
        for(int i=1;i<=n;i++)
         for(int j=1;j<=m;j++)
          {
            dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            if(a1[i]==a2[j])
            dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
            //因为更新,所以++; 
          }
        cout<<dp[n][m];
    }
    View Code

    而对于洛谷P1439而言(两个序列为1-n的两个全排列),不仅是卡上面的朴素算法,也考察到了全排列的性质:

    对于这个题而言,朴素算法是n^2n2的,会被10^5105卡死,所以我们可以考虑nlognnlogn的做法:

    因为两个序列都是1~n1 n的全排列,那么两个序列元素互异且相同,也就是说只是位置不同罢了,那么我们通过一个mapmap数组将AA序列的数字在BB序列中的位置表示出来——

    因为最长公共子序列是按位向后比对的,所以a序列每个元素在b序列中的位置如果递增,就说明b中的这个数在a中的这个数整体位置偏后,可以考虑纳入LCSLCS——那么就可以转变成nlognnlogn求用来记录新的位置的map数组中的LISLIS。

    上述为离散化处理

    #include<iostream>
    #include<cstdio>
    using namespace std;
    int a[100001],b[100001],map[100001],f[100001];
    int main()
    {
        int n;
        cin>>n;
        for(int i=1;i<=n;i++){scanf("%d",&a[i]);map[a[i]]=i;}
        for(int i=1;i<=n;i++){scanf("%d",&b[i]);f[i]=0x7fffffff;}
        int len=0;
        f[0]=0;
        for(int i=1;i<=n;i++)
        {
            int l=0,r=len,mid;
            if(map[b[i]]>f[len])f[++len]=map[b[i]];
            else 
            {
            while(l<r)
            {   
                mid=(l+r)/2;
                if(f[mid]>map[b[i]])r=mid;
                else l=mid+1; 
            }
            f[l]=min(map[b[i]],f[l]);
            }
        }
        cout<<len;
        return 0
    }
    View Code
  • 相关阅读:
    Python语法入门之基本数据类型
    ASCII,GBK,和Unicode的UTF-8,UTF-16,UTF-32阐述
    为什么计算机只认识0和1?
    Intel万兆网卡82599linux驱动安装
    Django自定义分页器
    Django图片防盗链
    Django配置用户上传文件夹和暴露后端文件夹资源
    Django之TruncMonth截取日期作为新的虚拟字段使用
    第十一篇:auth模块
    第十篇:跨站请求伪造csrf
  • 原文地址:https://www.cnblogs.com/bxd123/p/10527637.html
Copyright © 2020-2023  润新知