最长上升子序列
给你一个长度为N的序列,求其最长上升子序列的长度。
样例输入:
6
1 6 2 4 3 5
样例输出:
4
解释:其最长上升子序列的长度为4。可以是{1 2 4 5 }或者{1 2 3 5}
注意:子序列是不连续的。
假设我们把序列储存在a数组中,并且从下标1开始存储。
动态规划解法
定义 f[i] 表示到 a[i] 为止的最长上升子序列的长度。(其中a[i]必须被选中,必须以a[i]结尾)
所以动态转移方程是 f [ i ] = max( f [ j ] ) + 1. ( 1 <= j < i 并且 a [ j ] > a [ i ] )
也就是说 对于位置i来说,只要前面的a[ j ]小于a[ i ],那么就可以转移过来,从中选择一个最大的即可。
代码:
#include<iostream>
using namespace std;
const int maxn=10000;
int a[maxn],f[maxn];
int main() {
int n;
cin>>n;
for (int i=1; i<=n; i++)
cin>>a[i];
for (int i=1; i<=n; i++){ //n个数都得算f[i]
f[i]=1; //最起码是个1
for (int j=1; j<i; j++){ //前面的所有j
if (a[j]<a[i]) //满足小于
f[i]=max(f[i],f[j]+1); //选最大的
}
}
int ans=0;
for (int i=1; i<=n; i++) //统计答案
if (f[i]>ans) ans=f[i];
cout<<ans<<endl;
}
时间复杂度:双重循环,所以复杂度是O(n^2).
贪心+二分解法
对上述的动态规划算法考虑优化,外层循环,也就是每个数都得计算f[i],这个肯定没有办法再优化了。
所以优化的重点就在于内层循环找 i 前面的比 a[ i ] 小的最大的 f [ j ]。
说到查找,大家第一时间都会想到二分,但是二分要求数组得是有序的,f 数组不满足,那么我们可以使得 f 数组有序吗?
我们先把样例的 f 数组写出来,如下:
我们观察 f 数组划线的两个1,前一个表示的是以a[1]也就是1结尾的最长上升子序列长度是1,后一个表示以a[2]也就是6为结尾的最长上升子序列长度也是1。
这两个1对于后面的转移是有影响的,但是我们发现,只要是6能转移的,1肯定都能转移,所以这个6我们没有必要保存下来。
到这里我们可以总结出,在相同的长度下,我们想尽量让a[i]更小,这样状态更能转移到后面去,这种优化就是贪心思想的体现。比如刚刚说的样例,同样是长度为1,以1结尾比以6结尾好多了,所以我们选择1,而不用6,划线的4和3也是同样的情况。
所以,对于每个长度,我们只需要记录其最小的那个a[i]。 假设我们把这个数组称为g。
g[i]表示长度为 i 的最长上升子序列的最小结尾是 g[ i ]。g数组的长度就是截止当前的最长上升子序列的长度。
比如当前 g 数组有m个。
如果 a[i] >g[m] 说明可以用 a[i] 继续扩展.
如果 a[i]<g[m] 对于当前答案没有影响,但是 a[i] 可能会优化g数组,所以我们要在 g 数组中找到第一个小于 a[i] 的数,去优化g数组。因为g数组单调的,可以使用二分查找。
代码:
#include<iostream>
using namespace std;
const int maxn=10000;
int a[maxn],g[maxn];
int main() {
int n;
cin>>n;
for (int i=1; i<=n; i++)
cin>>a[i];
int m=0;
g[0]=-100000000; //初始化
for (int i=1; i<=n; i++){
if (a[i]>g[m]){ //满足就扩展
m++;
g[m]=a[i];
}else{ //否则二分
int l=0,r=m;
while (l!=r){
int mid=(l+r+1)/2;
if (g[mid]<=a[i]) l=mid;
else r=mid-1;
}
if (g[l]!=a[i]) g[l+1]=a[i]; //特判下相等的情况
}
}
cout<<m<<endl; //最长上升子序列的长度就是g数组的长度
}
其中,初始化g[0]为负值,目的是让a[1]一定可以选入。当然你可以选择先在g数组中加入a[1].
二分完毕后,有一个判断语句, if (g[l]!=a[i]) g[l+1]=a[i]; 因为我们求得是严格上升的,所以相同的情况下是不能更新g的。
答案就是g数组的长度。
时间复杂度:O(nlogn)。