算法描述:

给定一个序列a,求出最长的严格递增的子序列的长度。

子序列:在保证a中元素顺序不变的情况下,删除若干个元素得到的序列为子序列

一个很显然的思路:
我们考虑序列a1a_1aia_i

  • 当i=1时,该序列LIS长度显然为1,所以dp1dp_1=1。
  • 当i=2时,若a2a_2>a1a_1则,LIS长为2,即dp2dp_2=2,否则,LIS仍为1,即dp2dp_2=1。
  • 当i=3时,在a1a_1a2a_2之间寻找小于a3a_3的数,对于比a3a_3小的数,便可以考虑把a3a_3加入到序列中,从而LIS长度加一,即dp3dp_3=1+dpkdp_k
    …以此类推
  • 当i=k时,在a1a_1ak1a_{k-1}之间找出所有aia_i<aka_k,并取出max{dpidp_i},则dpkdp_k=max{dpidp_i}+1。

得到所有的dp后,dp中的最大值就是LIS的最大长度。

由此我们得到了LIS的递推求法:

1
2
3
4
5
6
7
8
9
10
11
12
for(int i=1;i<=n;i++)
{
dp[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
{
dp[i]=max(dp[i],dp[j]+1);
}
}
ans=max(ans,dp[i]);
}

分析其复杂度,显然该算法时间复杂度为O(n2n^2),空间复杂度为O(n)

对于O(n2n^2)的复杂度而言,当n>1e4的时候就非常难通过了,必须考虑更换算法。

分析上述递推算法,dpidp_i表示以aia_i为结尾的子序列中,最长的上升子序列的长度,我们更换两者的地位,即让dpidp_i的含义修改为:长度为i的子序列,该子序列以dpidp_i为结尾。

显然dpidp_i肯定越小越好,所以dpidp_i应该为所有可能值得最小值。因为对于同样长度为i的子序列中,末尾dpidp_i越小,那么这个子序列越有可能在后面加更多的数。那么最终的答案便是最大的i。

在实际例子中,我们能够发现dp数组是单调增的,为什么呢?以下用反证法简单证明:

如果dp不是单调增,即存在i < j,但是dpidp_i>=dpjdp_j,由于dpjdp_j存在,即存在长度为j的以dpjdp_j结尾的LIS,设该LIS为p,由于子序列上升,所以pip_i<pjp_j,取p序列前i个作为一个长度为i的LIS,dpidp_i应该为所有可能值得最小值,所以应该有dpidp_i<=pip_i,但是dpidp_i>dpjdp_j,显然矛盾,因此dp数组单调增。

那么知道这个性质之后,问题就好办多了。
我们遍历数组a:
举例a=3,2,4,5,3,7

  • 当i=1时,dpidp_i=3。{dp=3}
  • 当i=2时,由于aia_i=2小于dp1dp_1=3,那么dp1dp_1=2显然是更优的。{dp=2}
  • 当i=3时,我们可以直接将aia_i加入进dp后面,因为4>2。{dp=2,4}
  • 当i=4时,与前面同理。{dp=2,4,5}
  • 当i=5时,我们可以替换掉dp2dp_2,因为3<4,存在子序列{2,3}优于子序列{2,4}。{dp=2,3,5}

由此可见,我们对序列的操作实质上就是在dp中寻找比aia_i大(或等)的数,然后进行替换。
由于dp升序,这个操作可以用二分查找实现。
而对于未找到的情况,则说明dp中所有数据都小于aia_i,那么就可以延长dp。
最终的答案显然就是dp的长度。
于是我们就有了贪心+二分的算法:
分析其复杂度,显然时间复杂度为O(nlogn),空间复杂度为O(n)

Code

1
2
3
4
5
6
7
8
vector<int>dp;
for(int i=1;i<=n;i++)
{
auto pos=lower_bound(dp.begin(),dp.end(),a[i]);
if(pos==dp.end())dp.push_back(a[i]);
else *pos=a[i];
}
ans=dp.size();

总结

后者算法显著优于前者(笑~)