最长上升子序列

最长上升子序列

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\n", 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\n",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());
}

 

posted @ 2020-06-06 15:10  Jason66661010  阅读(285)  评论(0编辑  收藏  举报