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

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

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

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

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

  • 状态转移方程:
    f [ i ] = m a x ( f [ i ] , f [ j ] + 1 ) ; f[i]=max(f[i],f[j]+1); 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 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;

}
//不记录路径:
#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;

}
优化版
题目链接: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;

}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值