求解最长上升子序列通常做法都是直接dp,很容易得到dp状态转移方程:
d p [ i ] = m a x ( d p [ j ] ) + 1 dp[i]=max(dp[j])+1 dp[i]=max(dp[j])+1,其中 0 ≤ j < i 且 n u m [ j ] < n u m [ i ] 0≤j<i且num[j]<num[i] 0≤j<i且num[j]<num[i]
用两个循环遍历原数组,维护dp数组,遍历结束之后找到dp数组中的最大值就是最长上升子序列。代码如下:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int>dp(n,1);
dp[0]=1;
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]) dp[i]=max(dp[i],dp[j]+1);
}
}
return *max_element(dp.begin(),dp.end());
}
很明显时间复杂度是O(n²),在n较大时容易超出时间限制,因此我们考虑另一种方法求解LIS问题
贪心+二分查找求解LIS问题
如果我们想要上升的子序列尽可能的长,那么我们需要让序列上升的尽可能慢,即我们希望每次在上升子序列中加上的那个数尽可能的小。
基于上面的贪心思路,我们维护一个数组d[i],他表示长度为i的最长上升子序列的末尾元素的最小值,用length记录当前最长上升子序列的长度,起始时length=1,d[1]=nums[0];
同时我们可以注意到:d数组是单调递增的。我们依次遍历数组nums中的每个元素,并更新数组d和length的长度,如果nums[i]>d[len],,则更新length=length+1,否则在d[1…length]中找到满足d[i-1]<nums[j]<d[i]的下标i,并更新d[i]=nums[j];
因此整个算法的流程为:设当前已经求出的最长上升子序列的长度为length,从前往后遍历数组,在遍历到nums[i]时:
- 如果nums[i]>d[length],则直接加入d末尾,并length++;
- 否则,查找d中第一个大于nums[i]的元素,并将其替换。
以输入序列[0,8,4,12,2]为例:
- 第一步插入0,d=[0];
- 第二步插入8,d=[0,8];
- 第一步插入4,d=[0,4];
- 第一步插入12,d=[0,4,12];
- 第一步插入2,d=[0,2,12];
代码如下:
vector<int> d;
int length = 0;
for (int j = 0; j < n; j++) {
++length;
auto it = upper_bound(d.begin(), d.end(), arr[j]); //不递减子序列,若是严格递增则用lower
if (it == d.end()) {
d.push_back(arr[j]);
}
else {
*it = arr[j];
}
}