最长上升子序列(LIS)问题

(iBus的输入法真的难用啊。。。用起来的感觉就像被电脑和键盘cj/(ㄒoㄒ)/~~,写到一半忍不住去换了fcitx+搜狗,然而可能是我的使用姿势不对,总出BUG,不过现在终于对上号了owo~)

 

这个问题让我想了N久N久N久,深深地感到智商欠费怀疑人生,我真是应该自挂东南枝。。


      问题:给出一个序列 例如(2 1 5 3 6 4 8 9 7),求该序列的最长上升子序列长度(1 3 4 8 9,长度为5)。

最长上升子序列问题,常用解法有两种,都用到了动态规划的思想,一种是直接用动态规划,比较好想,但是算法复杂度为O(n^2),另一种算法进行了优化,把时间复杂度降低到了O(nlogn),此两种方法本文都进行讲解,先看第一种

  • 通称O(n^2)算法

         设a为存储序列的数组,设f[i]为以i结尾最长子序列的长度,那么如何递推出f[i]的值呢?我们设一个变量j来表示,1<=j<=i-1,对于所有的a[j],只有当a[j]<a[i]的时候,我们才需要判断是否需要把a[i]添加到f[j]中,当然我们需要注意,f[i]的以i结尾的最长子序列的长度,所以我们要在所有可以添加a[i]的子序列中,找到最长的那个子序列,然后我们把a[i]添加进子序列中,另f[i]=f[j]+1即可。

         我在某篇博客上看到了f[i]=max(f[j](0<=j<i))+(a[i]>a[j]?1:0) 这样的公式,仔细研究这个公式其实是有BUG的,因为他找到的子序列max(dp[j])不一定能满足a[i]可以被添加进去,这点要注意仔细思考,不然很容易就出错。

         正确的推理公式应该是:f[i]=max( f[j] | (1<=j<=i-1,a[i]>a[j]) ) + 1

         我觉得这个算法是达不到O(n^2)的复杂度的。

代码:

int lis1(int a[],int n)
{
    int *f=new int[n+1];
    for(int i=0;i<=n;i++){f[i]=0;}//初始化
    int Max;
    for (int i = 1; i <= n; ++i){
        Max = 0;
        for (int j = 1; j <= i-1; ++j){
            if (a[i] > a[j]){
                Max = max(Max, f[j]);//找到可以把a[i]作为结尾的最长子序列
            }
        }
        f[i] = Max + 1;//把a[i]加入到这个子序列中
    }
    Max = 0;
    for (int i = 1; i <= n; ++i){//找到最长上升子序列
        if (f[i] > Max)    Max = f[i];
    }
    return Max;
}
  • O(nlogn算法)

         分析上面的算法发现,每次都要从i-1个数里面遍历,再找到最长的子序列,这就提高了计算时间,那么如何优化呢?是否有一种办法把这个寻找的过程简化?

        我们使用b[i]来记录长度为i的上升子序列的末尾元素的最低值 ,这是什么意思?长度为i的子序列可能有很多个,而我们为了寻找整个序列的最长上升序列,只需要关注长度为i的序列末尾元素的最低值,因为末尾元素的值越低,我们越可能得到更长的上升子序列,这是一种贪心算法的思想,显然b数组的递增的。用反证法简单地证明一下,假若存在i<j,b[i]=a,b[j]=b,且a>b,那么长度为j的上升序列里面一定包含一个长度为i的子上升序列,且b[i]<b[j],与假设矛盾。

         我们假设b最开始是没有长度的,通过依次读取每一个a[i]来更新b的长度以及b数组中存储的值。

         当我们读取到的a[i] 大于当前最长子序列的末尾元素最低值,那么a[i]可以被加入到这个子序列中,我们把当前最长子序列长度+1,并且更新b数组的值。

         如果条件不成立,那么我们无法添加a[i]到当前最长的子序列中,这时候我们用a[i]更新长度不是最长的子序列末尾元素的最低值,如何更新呢,我们在b数组中从前向后找到 第一个大于a[i]的数,然后用a[i]去更新这个值,这是什么诡异操作?假如b[1-5]=1 3 4 6 9,a[i]=5,那么a[i]可以加入的子序列就只有b[1-3]=1 3 4,不过我们既然要求最长子序列,那就让a[i]并入长度为3的子序列,并入后长度为4,这时我们要更新b[4]的值,忘记b存储的是什么了嘛?是长度为i的上升子序列的末尾元素的最低值,a[i]是小于原来的b[4]的,所以把a[i]并入长度为3的子序列之后及时用a[i]更新b[4]的值。

         如果对这里的操作还不是很明白的话,可以自己模拟一遍,在纸上写一写画一画~也可以点击这个链接:https://www.felix021.com/blog/read.php?1587 ,看一遍他的模拟过程。

下面放代码:

int lis2(int a[],int n){
    int ans=0;//假设b最开始的长度为0
    int *b=new int[n+1];
    for(int i=0;i<=n;i++){b[i]=0;}//初始化b的值
    for(int i=1;i<=n;i++){//依次读取a[i]的值并更新数组b
        if(b[ans]<a[i]){//如果可以被添加到长度为ans的子序列里面
            b[++ans]=a[i];//添加并且ans++;
            continue;
        }else{
            b[insert_x(b,ans,a[i])]=a[i];//否则就使用a[i]更新b数组的值
        }
    }
    delete []b;
    return ans;//最后数组的长度ans就是答案。
}
int insert_x(int b[],int n,int x){//寻找插入点,插入第一个大于x的位置,b是数组,查找范围是[0,n]
    int mid=0;
    int front=0;
    int rear=n;
    while(front!=rear-1){//使用二分查找
        mid=(front+rear)/2;
        if(x>b[mid]){
            front=mid;
        }else{
            rear=mid;
        }
    }
    return front+1;
}

注意这个算法为什么比第一个算法快,因为b是递增的数列,所以查找插入点的时候可以使用二分查找,二分查找的速度远远快于遍历整个数组,所以,这就体现了数据结构对算法的重要程度。

下面贴一个完整可以运行的代码:

#include <iostream>
#include <cstdlib>
#include <algorithm>
using namespace std;
int lis1(int a[],int n);
int lis2(int a[],int n);
int insert_x(int b[],int n,int x);//在前N个数据里面,返回应该插入的位置
int main(){
    int n=0;
    cin>>n;
    int *a=new int[n+1];
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    cout<<lis1(a,n)<<endl;
    cout<<lis2(a,n)<<endl;
    delete []a;
    return 0;
}
int lis1(int a[],int n)
{
    int *f=new int[n+1];
    for(int i=0;i<=n;i++){f[i]=0;}//初始化
    int Max;
    for (int i = 1; i <= n; ++i){
        Max = 0;
        for (int j = 1; j <= i-1; ++j){
            if (a[i] > a[j]){
                Max = max(Max, f[j]);//找到可以把a[i]作为结尾的最长子序列
            }
        }
        f[i] = Max + 1;//把a[i]加入到这个子序列中
    }
    Max = 0;
    for (int i = 1; i <= n; ++i){//找到最长上升子序列
        if (f[i] > Max)    Max = f[i];
    }
    return Max;
}
int lis2(int a[],int n){
    int ans=0;
    int *b=new int[n+1];
    for(int i=0;i<=n;i++){b[i]=0;}
    for(int i=1;i<=n;i++){
        if(b[ans]<a[i]){
            b[++ans]=a[i];
            continue;
        }else{
            b[insert_x(b,ans,a[i])]=a[i];
        }
    }
    delete []b;
    return ans;
}
int insert_x(int b[],int n,int x){
    int mid=0;
    int front=0;
    int rear=n;
    while(front!=rear-1){
        mid=(front+rear)/2;
        if(x>b[mid]){
            front=mid;
        }else{
            rear=mid;
        }
    }
    return front+1;
}

输入:第一行输入一个数字N,后面输入N个数字,使用tab、空格、回车进行间隔

输出:使用两个算法进行计算的最长上升子序列长度

样例:

    输入:

         9
         2 1 5 3 6 4 8 9 7

    输出:

         5

         5