今天在lintCode上面做了一道关于二分法的题,觉得有必要记录下来。
1.概览
(1).题意
给定一个整数序列,找到最长上升子序列(LIS),返回LIS的长度。
(2).说明
最长上升子序列的定义:
最长上升子序列问题是在一个无序的给定序列中找到一个尽可能长的由低到高排列的子序列,这种子序列不一定是连续的或者唯一的。
(3).样例
给出 [5,4,1,2,3],LIS 是 [1,2,3],返回 3
给出 [4,2,4,5,3,7],LIS 是 [2,4,5,7],返回 4
(4).挑战
要求时间复杂度为O(n^2) 或者 O(nlogn)
2.解题思路
最长上升子序列的经典解法是动态规划,也是最简单的解法,但是动态规划的时间复杂度是O(n^2),说实话,还是有点高。本文也不会讲解动态规划的解法,但是会贴出代码,本文的重点在于讲解二分法如何解决这个问题。
二分法的解决问题的关键在于,开辟一个数组,用来存储当前形成长度为k序列的最后一个元素。例如,我们假设数组为a[],a[k]存储的就是最长序列为k+1,在nums数组中的最后一个元素。可能有人还是看不太懂,这里举一个实际的例子。
假设数组nums[] = {5,4,1,2,3},数组a[]用来存储当前形成长度为k序列的最后一个元素。则a[0] = 1, a[1] = 2, a[2] = 3。其实我们在遍历数组nums时,最开始a[0] = 5,然后a[0] = 4, 最后a[0] = 1。
通过这个特性,我们知道a数组里面的存储的数字是单调递增的,那么当我们遍历nums数组的第i个数字,如果a[len - 1] < nums[i]的话,那么a[++len] = nums[i](len表示当前最长的序列的长度);反之,我们使用二分法查找法查找a数组,找到从左往右第一个大于等于nums[i]的位置,将这个位置上面的元素更新为nums[i]就行了,这样能保证整个a数组是单调递增的。
最终len就是最长上升序列的长度。
3.代码
理解了上面的思路,下面的二分法代码就非常容易理解。这里先贴出动态规划的代码。
(1).动态规划
public int longestIncreasingSubsequence(int[] nums) {
int a[] = new int[nums.length];
a[0] = 1;
int max = Integer.MIN_VALUE;
for(int i = 1; i < nums.length; i++) {
int num = Integer.MIN_VALUE;
for(int j = i - 1; j >= 0; j--) {
if(nums[j] < nums[i]) {
num = Math.max(num, a[j] + 1);
}else {
num = Math.max(num, 1);
}
}
a[i] = num;
max = Math.max(num, max);
}
return max;
}
(2).二分法
public int longestIncreasingSubsequence(int[] nums) {
if(nums.length == 0){
return 0;
}
int a[] = new int[nums.length];
//初始化a[0]
a[0] = nums[0];
int len = 0;
for(int i = 1; i < nums.length; i++) {
//如果当前nums的数字大于a[len](a数组有值的最后一个元素)
//那么a[++len] = nums[i]
if(nums[i] > a[len]) {
a[++len] = nums[i];
}else {
//反之使用二分查找法,查找从左往右第一个大于等于nums[i]的位置
//然后将该位置更新为
int index = binarySearch(a, len, nums[i]);
a[index] = nums[i];
}
}
//记得这里是len + 1,因为len是从0开始的
return len + 1;
}
private int binarySearch(int a[], int len, int ans) {
int low = 0;
int height = len;
while(low <= height) {
int mid = (height + low) / 2;
if(a[mid] >= ans) {
height = mid - 1;
}else {
low = mid + 1;
}
}
return low;
}