加载中...

【题解】最长上升子序列(记录路径+优化版本)


题目链接:895. 最长上升子序列 - AcWing题库

题意:

给定数列,找到最长子序列。

思路:

  • 动态规划。数组a[N]存数列。

  • 状态表示:f[i]表示以a[i]结尾的上升子序列的集合中长度的最大值。

  • 状态转移方程:f[i]=max(f[i],f[j]+1);

    • 因为f[i]表示以a[i]结尾,既然序列一定包含a[i],那么我们不妨先看前i-1个数中的子序列集合中,有多少能和a[i]匹配,然后找到所能匹配的集合中的最大值。
  • 为什么可以保证g[N]中存的一定是最长子序列的路径?

  • 关键在 if(f[i]<f[j]+1) 这句,通过这句if判断,保证了在更新f[i]时一定是比之前所有以a[i]为结尾的子序列长度更大,而g[i]也随之更新。如果我们能保证g[i]对于每个以a[i]为结尾的子序列集合所存的都是最优解,那么通过g[N]所能找到的子序列就一定是最长的子序列。

  • 动态规划本质上其实是一种集合的思想。

不记录路径代码:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1010;

int n;
int a[N],f[N];//f[i]表示以a[i]结尾的子序列长度的集合中的最大值

int main()
{
    cin>>n;
    for(int i=1;i<=n;++i) cin>>a[i];
    

    for(int i=1;i<=n;++i) f[i]=1;
    
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<i;++j)
            if(a[i]>a[j])f[i]=max(f[i],f[j]+1);
    }
    
    int res=0;
    for(int i=1;i<=n;++i) res=max(res,f[i]);
    cout<<res;
    
    return 0;

}

记录路径代码:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1010;

int n;
int a[N],f[N];//f[i]表示以a[i]结尾的子序列长度的集合中的最大值
int g[N];//g[i]以a[i]为结尾的子序列是由谁转移过来的
int p[N];

int main()
{
    cin>>n;
    for(int i=1;i<=n;++i) cin>>a[i];
    

    for(int i=1;i<=n;++i) f[i]=1;//初始化,开始时所有的序列长度为1
    
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<i;++j)
            if(a[i]>a[j])//f[i]=max(f[i],f[j]+1);
                if(f[i]<f[j]+1)
                {
                    g[i]=j;
                    f[i]=f[j]+1;
                }
    }
    
    int k=1;
    for(int i=1;i<=n;++i)
    {
        if(f[i]>f[k])
            k=i;
    }
    
    cout<<f[k]<<endl;
    int len=f[k];
    for(int i=1;i<=len;++i)
    {
        p[i]=a[k];
        k=g[k];
    }
    
    for(int i=len;i>=1;-- i) cout<<p[i]<<" ";
    return 0;

}

优化版

题目链接:896. 最长上升子序列 II - AcWing题库

题意:

同上。

思路:

  • 通过 f[i]=max(f[i],f[j]+1) 来计算最长上升子序列会造成大量的无用计算。
    • 比如: 3 2 1 2 5 8 。 3、2和1的长度都是1,但是我们显然没有必要去重复遍历3和2,因为所有能接在3和2后面的
      都能接在1后面。
  • 那么如何去简化这种机械的重复呢?
    • 既然3、2和1同为长度为1的子序列,而且3和2可以完全被1替代,那么对于长度的为1的子序列我们只记录1就可以,如果有一个数比1大,那么就让他接在1后面组成长度为2的子序列。
      对于每个长度的子序列都记录一个尾元素最小值,就组成了集合f。
      对于原数列的每个元素a[i]我们只需要在集合f中找到小于a[i]的最大值f[j],并且直接接在f[j]后面就可以,
      也即 f[j+1]=a[i] ;
  • 为什么可以直接接在后面?
    • 因为集合f是由不同长度子序列尾元素的最小值构成的,如果有f[i+1]<f[i],那么对于f[i]一定有f[i+1]更适合,与我们对集合f的定义相悖,所以对于集合f,一定有 f[i+1]>=f[i] ,而我们定义f中的子序列一定是上升子序列,所以一定有 f[i+1]>f[i]
      因为 a[i]>f[i] ,且 a[i]<f[i+1] ,所以可以直接用a[i]更新f[i+1]的值。
  • 怎么在集合f中找到小于a[i]的最大值?
    • 我们已经证明集合f一定是上升的,所以可以用二分的方式。

代码:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=100100;

int a[N],f[N];//f[i]表示长度为i的子序列尾元素的最小值
int n;

int erfen(int l,int r,int k)
{
    while(l<r)
    {
        int mid=l+r+1>>1;
        if(f[mid]<k)l=mid;//因为是写的l==mid,所以在算mid时要向上取整  ->  mid=l+r+1>>1;  防止边界出错
        else r=mid-1;
    }
    return l;
}

int main()
{
    cin>>n;
    for(int i=0;i<n;++i) cin>>a[i];

    f[0]=-2e9;//因为数据范围是-1e9~1e9,所以要初始化一个边界
    int ans=0;//子序列集合的最大长度,也就是二分的范围
    for(int i=0;i<n;++i)
    {
        int x=erfen(0,ans,a[i]);
        ans=max(ans,x+1);
        //因为f[x]是小于a[i]的最大值,所以f[x+1]一定大于等于a[i],可以直接更新f[x+1]
        f[x+1]=a[i];
    }
    
    cout<<ans;
    
    return 0;

}

文章作者: 心意
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 心意 !
评论
  目录