zoukankan      html  css  js  c++  java
  • 最长上升子序列

    最长上升子序列

    Longest Increasing Subsequence LIS

    本文参考(https://blog.csdn.net/lxt_Lucia/article/details/81206439

    子串与子序列的区别

    (1)字符子串指的是字符串中连续的n个字符,如abcdefg中,ab,cde,fg等都属于它的字串。

    (2)字符子序列指的是字符串中不一定连续但先后顺序一致的n个字符,即可以去掉字符串中的部分字符,但不可改变其前后顺序。如abcdefg中,acdg,bdf属于它的子序列,而bac,dbfg则不是,因为它们与字符串的字符顺序不一致。

    LIS的不唯一性

    对于固定的数组,虽然LIS序列不一定唯一,但LIS的长度是唯一。再拿我们刚刚举的栗子来讲,给出序列 ( 1, 7, 3, 5, 9, 4, 8),易得最长上升子序列长度为4,这是确定的,但序列可以为 ( 1, 3, 5, 8 ), 也可以为 ( 1, 3, 5, 9 )。

    LIS的求解方法

    方法一:动态规划O(n2)

    将问题分为较小的子问题:我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。

    例子:

    让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。

      前1个数 d(1)=1 子序列为2;

      前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7

      前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1

      前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5

      前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6

      前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4

      前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3

      前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8

      前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9

      d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5

    状态转移方程:

    F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])

    代码模板:

    for (int i = 0; i < n; i++)
            for (int j = 0; j < i; j++)
                if(list[i]>list[j])
                    dp[i] = max(dp[i], dp[j] + 1);

    代码:

    #include<iostream>
    #include<algorithm>
    using namespace std;
    int list[100];
    int dp[100];
    int main()
    {
        int n;
        scanf("%d", &n);
        for (int i = 0; i < n; i++)
        {
            scanf("%d", &list[i]);
            dp[i] = 1;
        }
        for (int i = 0; i < n; i++)
            for (int j = 0; j < i; j++)
                if(list[i]>list[j])
                    dp[i] = max(dp[i], dp[j] + 1);
        int maxx = -1;
        for (int i = 0; i < n; i++)
            maxx = max(maxx, dp[i]);
        printf("%d", maxx);
    
    }

    输出LIS路径的代码:

    思路:使用path标记前一个节点在list数组的下标

    #include<iostream>
    #include<algorithm>
    #include<stack>
    using namespace std;
    int list[100];
    int dp[100];
    int path[100];
    int main()
    {
        int n;
        int maxnum=-1, maxi;
        scanf("%d", &n);
        for (int i = 0; i < n; i++)
        {
            scanf("%d", &list[i]);
            dp[i] = 1;
            path[i] = -1;
        }
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < i; j++)
            {
                if (list[i] > list[j]&&dp[i]<dp[j]+1)
                {
                    dp[i] = dp[j] + 1;
                    path[i] = j;
                }
            }
            if (dp[i] > maxnum)
            {
                maxnum = dp[i];
                maxi = i;
            }
    
        }
        stack<int>out;
        printf("%d
    ", maxnum);
        while (maxi != -1)
        {
            out.push(list[maxi]);
            maxi = path[maxi];
        }
        while (!out.empty())
        {
            printf("%d ", out.top()); out.pop();
        }
    }

    方法二:贪心+二分查找 0(log(n))

    新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。 那么,怎么维护 low 数组呢? 对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。具体方法是,在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。

    但是,但是!!!序列并不一定是正确的最长上升子序列!只是序列的个数是对的!

    代码

    #include <cmath>
    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #include<set>
    using namespace std;
    const int maxn = 300003, INF = 0x7f7f7f7f;
    int list[maxn];
    int n, ans;
    int main()
    {
        scanf("%d", &n);
        for (int i = 0; i <n; i++)
        {
            scanf("%d", &list[i]);
        }
        set<int>out;
        for (int i = 0; i < n; i++)
        {
             ans = list[i];
            auto it = out.lower_bound(ans);
            if (it != out.end()) out.erase(it);//不是最大的,所以不在最后,所以替换这个位置的数字,使它变小
            out.insert(ans);
        }
        printf("%d", out.size());
    }

    2.最长下降子序列 将原程序中的set 改为 set>

    3.最长不下降子序列 将原程序中的set改为multiset,lower_bound改为upper_bound

    4.最长不上升子序列 综合2与3.

    关于lower_bound( )和upper_bound( )的常见用法

    https://blog.csdn.net/qq_40160605/article/details/80150252

    lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

    upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

    在从大到小的排序数组中,重载lower_bound()和upper_bound()

    lower_bound( begin,end,num,greater<type>() ):从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

    upper_bound( begin,end,num,greater<type>() ):从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

    数组用法:

    int list[100],a;
    lower_bound(list,list+10,a);

    STL用法:

    set<int>list;
    int a;
    auto it=list.lower_bound(a);

    方法三:树状数组方法

    我们再来回顾O(n^2)DP的状态转移方程:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])

    我们在递推F数组的时候,每次都要把F数组扫一遍求F[ j ]的最大值,时间开销比较大。我们可以借助数据结构来优化这个过程。

    用树状数组来维护F数组(据说分块也是可以的,但是分块是O(n*sqrt(n))的时间复杂度,不如树状数组跑得快)

    首先把A数组从小到大排序,同时把A[ i ]在排序之前的序号记录下来。然后从小到大枚举A[ i ],每次用编号小于等于A[ i ]编号的元素的LIS长度+1来更新答案同时把编号大于等于A[ i ]编号元素的LIS长度+1。因为A数组已经是有序的,所以可以直接更新。有点绕,具体看代码。

    还有一点需要注意:树状数组求LIS不去重的话就变成了最长不下降子序列了。

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstdlib>
    #include <cstring>
    #include <cmath>
    using namespace std;
    const int maxn =103,INF=0x7f7f7f7f;
    struct Node{
        int val,num;
    }z[maxn]; 
    int T[maxn];
    int n;
    bool cmp(Node a,Node b)
    {
        return a.val==b.val?a.num<b.num:a.val<b.val;
    }
    void modify(int x,int y)//把val[x]替换为val[x]和y中较大的数 
    {
        for(;x<=n;x+=x&(-x)) T[x]=max(T[x],y);
    }
    int query(int x)//返回val[1]~val[x]中的最大值 
    {
        int res=-INF;
        for(;x;x-=x&(-x)) res=max(res,T[x]);
        return res;
    }
    int main()
    {
        int ans=0;
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&z[i].val);
            z[i].num=i;//记住val[i]的编号,有点类似于离散化的处理,但没有去重 
        }
        sort(z+1,z+n+1,cmp);//以权值为第一关键字从小到大排序 
        for(int i=1;i<=n;i++)//按权值从小到大枚举 
        {
            int maxx=query(z[i].num);//查询编号小于等于num[i]的LIS最大长度
            modify(z[i].num,++maxx);//把长度+1,再去更新前面的LIS长度
            ans=max(ans,maxx);//更新答案
        }
        printf("%d
    ",ans);
        return 0;
    }

    例题

    https://www.luogu.com.cn/problem/P1439

    题目分析

    题目中要求的是最长公共子序列,但是最长公共子序列需要使用动态规划计算,而题目的数据量为100000,二维DP数组太大,所以需要寻找别的办法

    题目中两行数字是一个数从1到n的排列,也就是说这两行数字的种类是一样的,就是顺序不一样,所以我们可以将数字进行离散化,让第一行的数字变为升序,然后求第二行数字的最长上升子序列

    代码

    使用动态规划TLE版:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    int list[100001];
    int list2[100001];
    int dp[100001];
    int main()
    {
        int n;
        cin >> n;
        for (int i = 1; i <=n; i++)
        {
            int temp;
            scanf("%d", &temp);
            list[temp] = i;
            dp[i] = 1;
        }
        for (int i = 1; i <= n; i++)
        {
            int temp;
            scanf("%d", &temp);
            list2[i] = list[temp];
        }
        for (int i = 1; i <=n; i++)
            for (int j = 1; j <i; j++)
                if (list2[i]>list2[j])
                    dp[i] = max(dp[i], dp[j] + 1);
        int maxx = -1;
        for (int i = 1; i <=n; i++)
            maxx = max(maxx, dp[i]);
        printf("%d", maxx);
    }

    改进版:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cmath>
    #include<set>
    using namespace std;
    int list[100001];
    int list2[100001];
    int main()
    {
        int n,ans;
        cin >> n;
        for (int i = 1; i <= n; i++)
        {
            int temp;
            scanf("%d", &temp);
            list[temp] = i;
        }
        for (int i = 1; i <= n; i++)
        {
            int temp;
            scanf("%d", &temp);
            list2[i] = list[temp];
        }
        set<int>out;
        for (int i = 1; i <=n; i++)
        {
            ans = list2[i];
            auto it = out.lower_bound(ans);
            if (it != out.end()) out.erase(it);//不是最大的,所以不在最后,所以替换这个位置的数字,使它变小
            out.insert(ans);
        }
        printf("%d", out.size());
    }
  • 相关阅读:
    UVA 10391 STL容器的使用
    UVA 10763
    UVA 10935
    UVA 洪水
    UVA 1594 set 里面放queue
    关于STL 容器的嵌套使用, 小试牛刀
    丑数 UVA 136
    UVA 1368 DNA
    antd 上传文件控件使用方法(坑)
    mysql查询一条工单时间需要10秒。优化sql语句得以解决。
  • 原文地址:https://www.cnblogs.com/Jason66661010/p/13054793.html
Copyright © 2011-2022 走看看