概述:
关于LIS部分,本篇博客讲一下LIS的概念定义和理解,以及求LIS的三种方法,分别是O(n^2)的DP,O(nlogn)的二分+贪心法,以及O(nlogn)的树状数组优化的DP,最后附上几道非常经典的LIS的例题及分析。
1.动态规划
我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。求前n-1个数的最长上升子序列,可以通过求前n-2个数的最长上升子序列……直到求前1个数的最长上升子序列,此时LIS当然为1。
让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。
前1个数 d(1)=1 子序列为2;
前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7
前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1
前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5
前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6
前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4
前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3
前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8
前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9
d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5
总结一下,d(i)就是找以A[i]结尾的,在A[i]之前的最长上升子序列+1,当A[i]之前没有比A[i]更小的数时,d(i)=1。所有的d(i)里面最大的那个就是最长上升子序列。其实说的通俗点,就是每次都向前找比它小的数和比它大的数的位置,将第一个比它大的替换掉,这样操作虽然LIS序列的具体数字可能会变,但是很明显LIS长度还是不变的,因为只是把数替换掉了,并没有改变增加或者减少长度。但是我们通过这种方式是无法求出最长上升子序列具体是什么的,这点和最长公共子序列不同。
复杂度:O (n^2)
2.贪心+二分
新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。
那么,怎么维护 low 数组呢?
对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。具体方法是,在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
我们再举一个例子:有以下序列A[ ] = 3 1 2 6 4 5 10 7,求LIS长度。
我们定义一个B[ i ]来储存可能的排序序列,len 为LIS长度。我们依次把A[ i ]有序地放进B[ i ]里。
(为了方便,i的范围就从1~n表示第i个数)
A[1] = 3,把3放进B[1],此时B[1] = 3,此时len = 1,最小末尾是3
A[2] = 1,因为1比3小,所以可以把B[1]中的3替换为1,此时B[1] = 1,此时len = 1,最小末尾是1
A[3] = 2,2大于1,就把2放进B[2] = 2,此时B[ ]={1,2},len = 2
同理,A[4]=6,把6放进B[3] = 6,B[ ]={1,2,6},len = 3
A[5]=4,4在2和6之间,比6小,可以把B[3]替换为4,B[ ] = {1,2,4},len = 3
A[6] = 5,B[4] = 5,B[ ] = {1,2,4,5},len = 4
A[7] = 10,B[5] = 10,B[ ] = {1,2,4,5,10},len = 5
A[8] = 7,7在5和10之间,比10小,可以把B[5]替换为7,B[ ] = {1,2,4,5,7},len = 5
最终我们得出LIS长度为5,但是,但是!!!B[ ] 中的序列并不一定是正确的最长上升子序列。在这个例子中,我们得到的1 2 4 5 7 恰好是正确的最长上升子序列,下面我们再举一个例子:有以下序列A[ ] = 1 4 7 2 5 9 10 3,求LIS长度。
A[1] = 1,把1放进B[1],此时B[1] = 1,B[ ] = {1},len = 1
A[2] = 4,把4放进B[2],此时B[2] = 4,B[ ] = {1,4},len = 2
A[3] = 7,把7放进B[3],此时B[3] = 7,B[ ] = {1,4,7},len = 3
A[4] = 2,因为2比4小,所以把B[2]中的4替换为2,此时B[ ] = {1,2,7},len = 3
A[5] = 5,因为5比7小,所以把B[3]中的7替换为5,此时B[ ] = {1,2,5},len = 3
A[6] = 9,把9放进B[4],此时B[4] = 9,B[ ] = {1,2,5,9},len = 4
A[7] = 10,把10放进B[5],此时B[5] = 10,B[ ] = {1,2,5,9,10},len = 5
A[8] = 3,因为3比5小,所以把B[3]中的5替换为3,此时B[ ] = {1,2,3,9,10},len = 5
最终我们得出LIS长度为5。但是,但是!!这里的1 2 3 9 10很明显并不是正确的最长上升子序列。因此,B序列并不一定表示最长上升子序列,它只表示相应最长子序列长度的排好序的最小序列。这有什么用呢?我们最后一步3替换5并没有增加最长子序列的长度,而这一步的意义,在于记录最小序列,代表了一种“最可能性”,只是此种算法为计算LIS而进行的一种替换。假如后面还有两个数据12和15,那么B[ ]将继续更新。
代码:
#include <cstdio>
#include <cstring>
#include <stack>
#include <queue>
#include <map>
#include <set>
#include <vector>
#include <cmath>
#include <algorithm>
#include<bits/stdc++.h>
using namespace std;
const double N = 1e6+10;
const double pi = acos(-1.0);
const int INF = 0x3f3f3f3f;
const int MOD = 1000000007;
#define ll long long
#define CL(a,b) memset(a,b,sizeof(a))
#define MAXN 100010
int a[MAXN];
int g[MAXN], d[MAXN];
int main()
{
int n;
cin >> n;
for(int i = 1 ; i <= n ; i++)
{
cin >> a[i];
}
vector<int>v; //用于存储第i+1长度的LIS的最后一位(长度为i的LIS结尾元素的最小值)
v.push_back(a[1]);
for(int i = 2;i <= n;i++){
if(a[i]<=*v.rbegin()){//反向迭代 (就是最后一个元素)
vector<int> :: iterator pos;
pos = lower_bound(v.begin(),v.end(),a[i]);
*pos = a[i];
}else{
v.push_back(a[i]);
}
}
cout << v.size() << endl;
return 0;
}
****a[i]<=*v.rbegin()是严格上升的,a[i]<*v.rbegin()是单调不减的
3.树状数组的维护
我们再来回顾O(n^2)DP的状态转移方程:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])
我们在递推F数组的时候,每次都要把F数组扫一遍求F[ j ]的最大值,时间开销比较大。我们可以借助数据结构来优化这个过程。用树状数组来维护F数组(据说分块也是可以的,但是分块是O(n*sqrt(n))的时间复杂度,不如树状数组跑得快),首先把A数组从小到大排序,同时把A[ i ]在排序之前的序号记录下来。然后从小到大枚举A[ i ],每次用编号小于等于A[ i ]编号的元素的LIS长度+1来更新答案,同时把编号大于等于A[ i ]编号元素的LIS长度+1。因为A数组已经是有序的,所以可以直接更新。有点绕,具体看代码。
还有一点需要注意:树状数组求LIS不去重的话就变成了最长不下降子序列了。
最长不下降子序列:
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn =103,INF=0x7f7f7f7f;
struct Node{
int val,num;
}z[maxn];
int T[maxn];
int n;
bool cmp(Node a,Node b)
{
return a.val==b.val?a.num<b.num:a.val<b.val;
}
void modify(int x,int y)//把val[x]替换为val[x]和y中较大的数
{
for(;x<=n;x+=x&(-x)) T[x]=max(T[x],y);
}
int query(int x)//返回val[1]~val[x]中的最大值
{
int res=-INF;
for(;x;x-=x&(-x)) res=max(res,T[x]);
return res;
}
int main()
{
int ans=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&z[i].val);
z[i].num=i;//记住val[i]的编号,有点类似于离散化的处理,但没有去重
}
sort(z+1,z+n+1,cmp);//以权值为第一关键字从小到大排序
for(int i=1;i<=n;i++)//按权值从小到大枚举
{
int maxx=query(z[i].num);//查询编号小于等于num[i]的LIS最大长度
modify(z[i].num,++maxx);//把长度+1,再去更新前面的LIS长度
ans=max(ans,maxx);//更新答案
}
printf("%d\n",ans);
return 0;
}