Longest Increasing Subsequence,LIS初步


Description

给定一个长度为n的序列,从中选出一个子序列(不一定要连续,但是相对位置不能变化)。如1 2 3的子序列有{1,2,3} {1,2} {1,3} {2,3} {1,2,3}。{3,1}不是它的子序列。
要求这个子序列的元素是单调递增的。求出最长的满足条件的子序列。

LIS是经久不衰的dp入门题之一,本文通过对不同数据,不同复杂度的分析,来剖析LIS。
特别感谢小C大人的耐心讲解以及大犇shuangde800 O(nlogn) 算法的分析。


不同的数据范围有不同的解法:

一、 n <=1000,ai<=1000

1) 最简单的dfs

我们可以发现这其实是一个取还是不取的问题:如果可以取这个比它小的数,那就取它,增加当前取的个数。如果当前数为a,位于x,则[1,x-1]中所有小于a的数都会被取到一遍:

int ans=0;
void dfs(int x,int sum){//当前数为x,已取sum个 
    if(sum>ans)ans=sum;
    for(int i=x-1;i>=1;i--)
        if(a[i]<a[x])dfs(i,sum+1); 
}

上面这种方法显著的特点是从后往前枚举,而且整体思路和通常的dfs无异,可谓暴力递归;同时,这种方法无法转化成LIS的通常dp解法,因为缺少决策。这里的决策其实本质是一样的,但略有出入:在所有符合题意的数中,找到最大值。而上述代码并没有体现出这点。

//int dp[M];
int dfs(int x){
    int mx=0;
//  if(dp[x])return dp[x];
    for(int i=x-1;i>=1;i--)
        if(a[i]<a[x]){
            int tmp=dfs(i);
            if(tmp>mx)mx=tmp;//决策之处
        }
    return mx+1;
//  return dp[x]=mx+1;
}

这样就可以产生决策,并且转化成类似dp的形式;同样,此时为了避免重复子序列,就可以进行记忆化搜索,如注释中的内容。

标准的记忆化dp写法 O(n2)

int dp[M],ans=0;
void Dp(){//以i结尾的序列,可以取到的最长序列长度
    for(int i=1;i<=n;i++){
        int mx=0;
        for(int j=1;j<i;j++)
            if(a[j]<a[i]&&dp[j]>mx)mx=dp[j];
        dp[i]=mx+1;
        ans=max(ans,dp[i]);
    }//O(n^2)
}

2) 变换一下思路

刚才定义的dp[i]的意义是“以i结尾的序列,可以取到的最长序列长度”,考虑到 ai 的范围也在1000以内,我们可以有这样一个dp[i]:

  • 以权值 ai 结尾的序列,在 [1,i1] 可以取到的最长序列长度。

这样就有下面这个dp:

void Dp(){//以i这个权值结尾的序列,可以取到的最长序列长度
    for(int i=1;i<=n;i++){
        int mx=0;
        for(int j=0;j<a[i];j++)
            if(dp[j>mx])mx=dp[j];
        dp[a[i]]=mx+1;
        ans=max(ans,mx+1);
    }//O(n^2)
}

这个dp中dp维护的是对应权值在[1,x]段中的最大子序列长度。
可以证明的是,若 ai=aj=x(i<j) ,则 dpidpj ,所以 dp[a[i]] 可以直接等于 mx+1

3) 下标与内部数据的交换

前面两种dp都是维护[1,x]中的最长子序列长度,而决策的因素是x或者a[x]。如果将两者对调一下呢?我们就会得到新的算法 O(nlogn)

  • dp[len] 长度为 len 的最长子序列末尾的最小权值。

现在想想,我们用到了单调栈来实现 O(logn) 的转移复杂度。在单调栈中,序列长度是在依次增加的,而序列的末尾权值也是在依次变大的。对于 ai ,它能接在所有末位元素权值 ai 的后面,而通过单调栈我们已经得到所有符合条件的可能序列长度,之后通过二分查找就可以找到最大满足序列。

#include<cstdio>
#include<algorithm>
#define M 1005
using namespace std;
int a[M],dp[M];
int main(){
    int n,len=1;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    dp[1]=a[1];
    for(int i=2;i<=n;i++)
        if(a[i]>dp[len])dp[++len]=a[i];//长度增加
        else{
            int p=lower_bound(dp+1,dp+1+len,a[i])-dp;
            dp[p]=a[i];
        }
    printf("%d\n",len);
    return 0;
}

4) 采用树状数组的神奇解法(16/9/26更新)

此时树状数组不是维护前缀和,而是维护该段前缀内的最大值。我们就把权值作为下标,维护[1,x-1]的dp最大值即可。由于会出现0的情况,我们还要把每个数据都+1。也是数据结构优化 O(n)O(logn)

#include <cstdio>
#include <cstring>
#define M 1005
#define lowbit(x) x&(-x)
int n,bit[M],a[M],ans=0;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);++a[i];
        int x=a[i]-1,m=0;
        while(x){
            if(bit[x]>m)m=bit[x];
            x-=lowbit(x);
        }++m;
        if(ans<m)ans=m;
        x=a[i];
        while(x<M){
            if(m>bit[x])bit[x]=m;
            x+=lowbit(x);
        }
    }printf("%d\n",ans);
}

二、 n <=105 ai <= 109

回到第二个Dp()

如果用“以i这个权值(即a[i])结尾的序列,可以取到的最长序列长度”去处理这个数据范围,有两个明显的困难与缺点:无法开到这么大的数组;数据过于分散,大量dp[]其实是被浪费的。

处理数据过于分散,我们需要在预处理中加入优化算法:离散化 O(nlogn)

int a[M],s[M];
int main(){
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
        s[i]=a[i];
    }
    sort(s,s+n);//使用的前提是数组已经有序
    int m=unique(s,s+n)-s;
//  for(int i=0;i<n;i++)if(s[m-1]!=s[i])s[m++]=s[i];//两种去重方法
    for(int i=0;i<n;i++)a[i]=lower_bound(s,s+n)-s;
}//O(nlogn);

运用离散化,相当于把a的范围重新缩小到 ai <= 103

这里写图片描述

这样lower_bound前后a数组的效果不变。

在第二个Dp()中的4、5行,发现这个操作只是在dp中寻找最大值,此时应该考虑用数据结构维护最大值的信息。我们可以采用线段树 O(nlogn) 做法:

#include<cstdio>
#include<algorithm>
using namespace std;
const int M=10005;
int a[M],s[M];
struct Segment{//叶子节点存储的是dp[a[i]],动态维护[1,x]的最大子序列长度
    struct Node{
        int L,R,mx;
        int mid(){return (L+R)>>1;}
    }tree[M<<2];
    void down(){}
    void up(int node){
        tree[node].mx=max(tree[node<<1].mx,tree[node<<1|1].mx);
    }
    void build(int L,int R,int node){
        tree[node].L=L;tree[node].R=R;tree[node].mx=0;
        if(L==R){
            scanf("%d",&a[L]);
            s[L]=a[L];
            return;
        }
        int mid=(L+R)>>1;
        build(L,mid,node<<1);
        build(mid+1,R,node<<1|1);
    }
    void updata(int x,int a,int node){
        if(tree[node].L==tree[node].R){
            tree[node].mx=max(tree[node].mx,a);
            return;
        }
        int mid=tree[node].mid();
        if(x<=mid)updata(x,a,node<<1);
        else updata(x,a,node<<1|1);
        up(node);
    }
    int query(int L,int R,int node){
        if(L>R)return 0;
        if(tree[node].L==L&&tree[node].R==R){
            return tree[node].mx;
        }
        int mid=tree[node].mid();
        if(R<=mid)return query(L,R,node<<1);
        else if(L>mid)return query(L,R,node<<1|1);
        else return max(query(L,mid,node<<1),query(mid+1,R,node<<1|1));
    }
}Tree;//封装了完整的线段树全部操作
int Dp(int n){
    int ans=0;
    for(int i=1;i<=n;i++){
        int k=Tree.query(1,a[i]-1,1);
        Tree.updata(a[i],k+1,1);
        ans=max(ans,k+1);
    }
    return ans;
}
int main(){
    int n;
    scanf("%d",&n);
    Tree.build(1,n,1);
    sort(s+1,s+n+1);
    int m=unique(s+1,s+n+1)-(s+1);
    for(int i=1;i<=n;i++)
        a[i]=lower_bound(s+1,s+m,a[i])-s;
    printf("%d\n",Dp(n));
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值