0%

最长上升子序列(LIS)

  • 给定长度为$N$的序列$a_i$,求序列中一个长度最长且递增的子序列。输出子序列的长度。$N\leq 1000$。

$O(N^2)$解法

  • 设$f[i]$为以$a_i$结尾的最长上升子序列的长度

    易知$f[i]$的最大值为 区间$[1,i)$中的 小于$a_i$的最大的$j$加上1

    意思就是,

    假设$a_j$为区间$[1,i)$中小于$a_i$的最大值,那么$a_i$就可以拼在$a_j$的后面,这时以$a_i$结尾的LIS的长度就为以$a_j$结尾的LIS长度+1

    用状态转移方程表示就是

    $\large{f[i]=\displaystyle\max_{j=1}^{i-1}(f[j])+1,a_i>a_j}$

  • 用代码表示就是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int A[N+1];
    int f[N+1];
    for(int i = 1;i<=N;i++){
    for(int j = 1;j<i;j++){
    if(A[i] <= A[j]) continue;
    f[i] = max(f[i],f[j]+1);
    }
    }
    int max_ = -1;
    for(int i = 1;i<=N;i++) max_ = max(f[i],max_);

$O(N\log N)$解法

  • 设$f[i]$为长度为$i$的上升子序列的最后一个数字

    设$a_j$为序列的第$j$个元素

    若$f{i}\leq a_j < f{i+1}$,则将$f_i$的值更新为$a_j$

    相当于将长度为$i$的上升子序列的最后一位$f_i$改为了更小的$a_j$,显然这样更有利于在后面接其他元素

  • 但是目前遍历长为$n$的序列,对于其中每项$a_j$,都需要进行至多$i$次操作去寻找合适的区间,时间复杂度仍然为$O(N^2)$

  • 但是,由于$f_i$单调递增,可以在查找的时候使用二分查找来优化,这样就至多进行$\log N$次查找,时间复杂度降为$O(N\log N)$

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int A[N+1];
    int f[N+1];
    int cnt = -1;
    //cnt代表当前最长上升子序列的长度
    fill(f+1,f+N+1,1e9);
    //fill(st,ed,val)意为用val填充数组[st,ed)区间
    //使用极大值填充区间
    for(int i = 1;i<=N;i++){
    int j = lower_bound(f+1,f+N+1,A[i]) - f;
    //lower_bound(st,ed,val)意为在[st,ed)区间内查找第一个不小于val的元素
    //lower_bound返回一个指向找到元素的迭代器
    //减去的f是地址
    //地址 - 地址 = 下标
    cnt = max(cnt,j);
    f[j] = A[i];
    }
    int ans = cnt;