题目链接: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]
;
- 既然3、2和1同为长度为1的子序列,而且3和2可以完全被1替代,那么对于长度的为1的子序列我们只记录1就可以,如果有一个数比1大,那么就让他接在1后面组成长度为2的子序列。
- 为什么可以直接接在后面?
- 因为集合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是由不同长度子序列尾元素的最小值构成的,如果有f[i+1]<f[i],那么对于f[i]一定有f[i+1]更适合,与我们对集合f的定义相悖,所以对于集合f,一定有
- 怎么在集合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;
}