问题描述
解决思路
O(n^2)
首先考虑使用动态规划的方法解决该问题。首先将原问题分解为子问题。对于长度为n的序列(从下标1开始),假设前n-1个元素形成了n-1个以arr[i]结尾的单调递增最长子序列,对于每个单调递增最长子序列,如果arr[n]>arr[i],第n个元素就可以添加到这个子序列的末尾,这个子序列的长度加一,n个元素的最大的单调递增子序列长度,就是更新后的前n-1个单调递增最长子序列中的最大长度。该问题具有最优子结构性质。根据最优子结构性质,可以得出递推公式:
if(arr[i] > arr[j]) dp[i] = max(dp[i], dp[j] + 1) 1≤j<i
求以arr[i]结尾的单调递增最长子序列,需要遍历arr[1]到arr[i-1]结尾的单调递增最长子序列的长度,而得到整个序列的单调递增最长子序列,需要求出以每个元素结尾的单调递增最长子序列的长度,结果为其中的最大值。因此该方法的时间复杂度上界为O(n^2)。
O(nlogn)
可以采用更好的方法解决单调递增最长子序列问题。在以上的解决方法中,计算dp[i]时,每次都要寻找到arr[i-1]为止的每个元素结尾的单调递增最长子序列的最大值,实际上这些可能产生最优解的子序列中,有一些是可以排除掉的,例如假设对于arr[x]和arr[y],它们的单调递增最长子序列分别为1,3,7和1,3,15,长度均为3,如果在这两个序列后面添加一些元素使其更长,一定是在1,3,7后面添加元素比在1,3,15后面添加元素得到的子序列更长,那么1,3,15这个序列就不需要在后续的更新中考虑了。从上面这个例子可以看出,只需要保留每个长度的单调递增子序列的最后一个元素,然后进行更新,寻找可能的更大长度的递增子序列。
可以使用一个tail数组保存尾元素,tail[i]表示长度为i的单调递增子序列的尾元素。从第一个元素开始更新时,tail[1]就是第一个元素的值,如果遇到了一个比tail[1]大的元素x,那么说明此时单调递增子序列的长度可以扩展为2,就令tail[2]=x,如果再遇到一个比tail[2]大的元素,说明单调递增子序列的长度可以扩展为3,按照这个规则去更新,显然tail会是一个单调递增的序列。如果遇到一个元素,不比当前的最大长度的末尾元素大,那么长度不可以拓展,但是这个元素可以更新某个特定长度i的tail[i],这个元素比tail[i]小,比tail[i-1]大,表示它可以补在长度i-1的子序列后面,又比现有的长度为i的子序列尾元素小。
通过逐个元素进行对tail的更新,已知最大长度的单调递增子序列的尾元素就可能变小,再遇到后面的元素时,就可能产生新的更长的单调递增子序列。在tail这个单调递增的数组中,可以使用二分法查找这个位置i,这样就降低了整体的时间复杂度到O(nlogn)。
代码实现
int LIS(int arr[], int len){
int tail[len+1];
int curMaxLen, updatePos;
curMaxLen = 0;
tail[0] = INT_MIN;
for(int i=1;i<=len;i++){
if(arr[i]>tail[curMaxLen]){
tail[++curMaxLen] = arr[i];
}
else{
updatePos = binarySearch(tail, 0, curMaxLen, arr[i]);
tail[updatePos] = arr[i];
}
}
return curMaxLen;
}
//二分查找
int binarySearch(int arr[], int left, int right, int x){
int mid;
while(left<=right){
mid = (left + right) / 2;
if(arr[mid]>x){
right = mid-1;
}
else{
left = mid+1;
}
}
return left;
}