加载中

最长不下降子序列(LIS)和最长公共子序列(LCS)算法

最长不下降子序列(LIS)

题目描述

给定一个长度为n的序列,求出它的最长不下降子序列长度

对于一个序列的子序列,可以理解为从原序列中删去若干的元素,剩下的数按照原来的先后顺序排列而成形成的序列;对于不下降子序列,即这个子序列中的元素值不递减(\(i < j , a[i] \leq a[j]\))

输入样例

7
1 2 3 -1 -2 7 9

输出样例

5

\(O(n^2)\)算法

我们考虑使用数组f[i]来表示以第i个元素为结尾的最长不下降子序列的长度,那么在状态转移时,我们要找出\([1,i-1]\)中所有小于或等于a[i]的元素f[j](满足不下降),然后找出这些元素所对应的f[j]中最大的那个进行转移,状态转移方程如下:

\[f[i] = \max \limits_{j < i, a[j] \leq a[i]} f[j]+1 \]

根据时间复杂度,这种算法能处理的n最大值约为\(10^4\),代码实现如下:

#include <iostream>
#include <vector>
using namespace std;

int n;
vector<int> a,f;

int main()
{
    cin>>n;a.resize(n+1),f.resize(n+1);
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        f[i]=1; // 如果没有能转移的状态,以a[i]结尾的最长不下降子序列长度为 1
        for(int j=1;j<i;j++)
            if(a[j]<=a[i]) // 满足序列的不下降性
                f[i]=max(f[i],f[j]+1); // 状态转移
    }
    cout<<f[n]<<endl; // f[n]就是最后所要的答案
    return 0;
}

\(O(n\log_2n)\)算法

使用之前的状态表示在进行决策时必须\(O(n)\)扫描之前的每一个元素,扫描的集合不存在单调性,也很难维护单调性,所以要实现对数优化,必须改变状态的表示方法

我们使用数组f[i]来表示长度为i的最长不下降子序列最小的结尾数值(这相当于将下标与值表示的内容进行了反转),可以发现,这样f数组是单调递增的,证明如下:

反证法,假设存在i<j,f[i]>f[j]的情况
由于f[i]是最小的,所以比i长的最长不下降子序列的结尾一定比f[i]要大,显然与假设矛盾
故任意i<j满足f[i]<=f[j],从而f数组是单调递增的

那么问题就转化为如何维护f数组的单调性

我们将f数组的上界存至变量len中,仍然从1到n对序列进行扫描,对于第i个元素,有以下几种情况:
第一种情况:a[i]>=f[len],说明当前的元素能与当前所能构成的最长不下降子序列构成一个更长的最长不下降子序列,那么我们执行f[++len]=a[i]
第二种情况:a[i]<f[len],说明当前元素不能与之前的最长不下降子序列构成一个更长的最长不下降子序列,但是它需要且仅需更新f数组中第一个大于a[i]的元素的值为a[i]

对于第一种情况很好理解,这里对第二种情况做出证明:

假设第一个大于a[i]的元素下标为d,则f[d]>a[i]&&f[d-1]<=a[i]
显然,可以让f[d]=a[i]
这样,由于f[d]>=f[d-1],所以仍然满足f[d]能从f[d-1]转移得到
又因为f[d]被赋值为a[i]后变小了,所以赋值后更优

对于d之前的所有元素,如果被赋值为a[i],那么就会导致元素值变大,与“最小”的要求不符
对于d之后的一个元素,如果被赋值为a[i],相当于对a[i]多使用了一次,与原序列不符
对于d之后一个元素后的所有元素,如果被赋值为a[i],那么不能维护f数组的单调性

注:此处的赋值可以认为是改变,如果赋值前后元素值未改变,那么赋值与不赋值等效

现在我们仅需要在f数组中二分查找第一个大于a[i]的元素并进行替换即可

upper_bound函数

upper_bound函数可以实现在一个有序数组中进行二分查找,以\(O(\log_2n)\)的时间复杂度得到答案:

template <class ForwardIterator, class T>
ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,const T& val);

根据时间复杂度,这种算法能处理\(10^6\)的数据量,由于读入数据量大,需要使用scanf或者快读,代码实现如下:

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;

int n,len;
vector<int> a,f;

inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int main()
{
    n=read();a.resize(n+1),f.resize(n+1);
    for(int i=1;i<=n;i++) a[i]=read();
    if(n==0) // 当n=0时需要进行特判
    {
        putchar('0');
        return 0;
    }
    f[++len]=a[1]; // 初始化
    for(int i=2;i<=n;i++)
    {
        if(a[i]>=f[len]) f[++len]=a[i]; // 如果要求最长上升子序列,将 ">=" 替换为 ">"
        else f[upper_bound(f.begin()+1,f.begin()+1+len,a[i])-f.begin()]=a[i]; // 如果要求最长上升子序列,将upper_bound替换为lower_bound
    }
    printf("%d",len);
    return 0;
}

树状数组

根据树状数组能在\(O(\log_2 n)\)的时间复杂度内实现单点修改和区间查询,还可以使用树状数组求最长不下降子序列:

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;

int n,cnt,ans;
vector<int> a,s,tree;

inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

inline int lowbit(int x)
{
    return x&-x;
}

void update(int k,int x)
{
    while(k<=n)
    {
        tree[k]=max(tree[k],x);
        k+=lowbit(k);
    }
}

int query(int k)
{
    int res=0;
    while(k)
    {
        res=max(res,tree[k]);
        k-=lowbit(k);
    }
    return res;
}

int main()
{
    n=read();
    a.resize(n+1),tree.resize(n+1);
    for(int i=1;i<=n;i++) a[i]=read();
    s=a;
    sort(s.begin()+1,s.begin()+1+n);
    cnt=unique(s.begin()+1,s.begin()+1+n)-s.begin(); // 离散化 
    for(int i=1;i<=n;i++)
    {
        int p=lower_bound(s.begin()+1,s.begin()+cnt,a[i])-s.begin();
        int t=query(p)+1; // 如果要求最长上升子序列则使用 query(p-1) 
        ans=max(ans,t);
        update(p,t);
    }
    printf("%d",ans);
    return 0;
}

最长公共子序列(LCS)

题目描述

给定一个长度为n的序列和一个长度为m的序列,求它们的最长公共子序列长度

输入样例

5 5
3 2 1 4 5
1 2 3 4 5

输出样例

3

\(O(nm)\)算法

使用二维数组f[i][j]表示第一个序列的前i个数与第二个序列的前j个数的最长公共子序列长度,那么有状态转移方程:

\[f[i][j]= \begin{cases} \max \{f[i-1][j],f[i][j-1],f[i-1][j-1]\}, & a[i] \ne b[i] \\ \max \{f[i-1][j],f[i][j-1],f[i-1][j-1],f[i-1][j-1]+1\}, & a[i]=b[i] \end{cases} \]

根据时间复杂度,这种算法能处理的n*m最大值约为\(10^8\),代码实现如下:

#include <iostream>
#include <vector>
using namespace std;

int n,m;
vector<int> a,b;
vector< vector<int> > f;

int main()
{
    cin>>n>>m;a.resize(n+1),b.resize(m+1),f.resize(n+1);
    for(int i=0;i<=n;i++) f[i].resize(m+1); // 这里从0-n都要进行resize 
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=m;i++) cin>>b[i];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            f[i][j]=max(f[i-1][j],f[i][j-1]);
            if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
        }
    cout<<f[n][m]<<endl;
    return 0;
}

对数级别算法

如果每一个序列中元素都互异,那么可以对之前的算法进行优化,我们从1到n扫描B序列,对于每个元素b[i],在A序列中寻找与之相等的元素a[j],若找到则将其加入到序列S中

于是我们得到了A序列的一个下标序列S,可以发现,求S序列的最长不下降子序列(或最长上升子序列)长度即为原两序列的最长公共子序列长度

那么如何在扫描B序列时得到与之相对应的A序列中的位置,这边使用二分的思想,为了使代码易于理解,这里使用map<int,int>来实现

实现代码如下:

#include <cstdio>
#include <algorithm>
#include <vector>
#include <map>
using namespace std;

int n,m,lens,lenf; // lens表示s的长度,lenf表示f的长度 
vector<int> a,b,s,f;
map<int,int> mp;

inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int main()
{
    n=read(),m=read();
    a.resize(n+1),b.resize(m+1),s.resize(m+1);
    for(int i=1;i<=n;i++) a[i]=read(),mp[a[i]]=i;
    for(int i=1;i<=m;i++)
    {
        b[i]=read();
        map<int,int>::iterator iter=mp.find(b[i]);
        if(iter!=mp.end()) s[++lens]=iter->second;
    }
    if(lens==0) // 特判 
    {
        putchar('0');
        return 0;
    }
    f.resize(lens+1);
    f[++lenf]=s[1]; // 初始化
    for(int i=2;i<=lens;i++)
    {
        if(s[i]>f[lenf]) f[++lenf]=s[i];
        else f[lower_bound(f.begin()+1,f.begin()+1+lenf,s[i])-f.begin()]=s[i];
    }
    printf("%d",lenf);
    return 0;
}

当序列中存在重复的元素时,也可以使用类似的方法,在扫描B序列时,对于每个元素b[i],我们要找到A序列中所有与之对应的元素位置,并且降序排列,针对这种情况,可以将map<int,int>改为map< int,vector<int> >,最后求出S的最长上升子序列(此时不能求最长不下降子序列会出错)

具体实现代码如下:

#include <cstdio>
#include <algorithm>
#include <vector>
#include <map>
using namespace std;

int n,m,len;
vector<int> a,b,s,f;
map< int, vector<int> > mp;

inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int main()
{
    n=read(),m=read();
    a.resize(n+1),b.resize(m+1);
    for(int i=1;i<=n;i++) a[i]=read(),mp[a[i]].push_back(i);
    for(int i=1;i<=m;i++)
    {
        b[i]=read();
        map< int, vector<int> >::iterator mp_iter=mp.find(b[i]);
        if(mp_iter!=mp.end())
        {
            for(vector<int>::reverse_iterator vec_iter=mp_iter->second.rbegin();
	        vec_iter!=mp_iter->second.rend();vec_iter++)
	        s.push_back(*vec_iter); // 按下标降序加入 
        }
    }
    if(s.empty()) // 特判 
    {
        putchar('0');
        return 0;
    }
    f.resize(s.size()+1);
    f[++len]=s[0]; // 初始化
    for(vector<int>::iterator iter=s.begin()+1;iter!=s.end();iter++)
    {
        if(*iter>f[len]) f[++len]=*iter;
        else f[lower_bound(f.begin()+1,f.begin()+1+len,*iter)-f.begin()]=*iter; // 要求最长上升子序列,必须使用lower_bound 
    }
    printf("%d",len);
    return 0;
}

参考资料

posted @ 2021-07-26 13:12  fenggwsx  阅读(415)  评论(0编辑  收藏  举报