O(NlogN)的动态规划解法
很容易就能想到,如果用
代表以
结尾的LIS长度,用
代表数组第
项,可以写出以下式子:
最大的
就是我们需要的解,代码如下:
public int findLongest(int[] array) {
// init
int n = array.length;
int[] dp = new int[n];
for (int i = 0; i < n; i++) dp[i] = 1;
// compute dp[i]
for (int i = 1; i < n; i++) {
int maxLen = 1;
for (int j = 1; j < i; j++) {
if (array[j] < array[i] && dp[j] > maxLen) {
maxLen = dp[j];
}
}
dp[i] = maxLen + 1;
}
// find max dp[i]
int lis = 1;
for (int i : dp) {
if (i > lis) lis = i;
}
return lis;
}
但是,由于计算dp过程中中间内层循环的存在,使得我们的算法复杂度变成了O(N^2),我们想,是否可以通过某种方式消去内层循环。实际上内层循环所作的事是尽可能扩展前面得到的LIS,要进行这样的扩展,需要满足已经提到的两个条件:
- A_j<A_i
也就是说对于每一个“新来的”array[i]
,要在符合上升序列的前提下,找到一个最长的可扩展序列去扩展,我们的内层循环就是进行一个这样的可扩展序列的搜索,那我们可能会想利用二分搜索之类的方式把这个搜索过程从O(N)优化到O(logN),由此可以得到更优的算法:
优化:O(NlogN)的算法
那么怎么找到一个可以扩展的序列呢?思路是这样的:
- 有没有长度为
1
的序列可供扩展?如果可以扩展,那么这个序列的末尾一定要比array[i]
要小,那么其实只需要找到所有长度为1
的序列中,末尾最小的那个,如果最小的末尾比array[i]
小那么一定可以扩展 - 有没有长度为
2
的序列可供扩展?如果可以扩展,那么这个序列的末尾一定要比array[i]
要小,那么其实只需要找到所有长度为2的序列中,末尾最小的那个,如果最小的末尾比array[i]
小那么一定可以扩展 - 有没有长度为
3
的序列可供扩展?如果可以扩展,那么这个序列的末尾一定要比array[i]
要小,那么其实只需要找到所有长度为3
的序列中,末尾最小的那个,如果最小的末尾比array[i]
小那么一定可以扩展
……
我们需要扩展上面那些可扩展序列中最长的那个,这可以通过引入一个辅助数组来通过二分搜索查到,这个辅助数组(叫minEnd[]
)中,minEnd[k]
存储长度为k - 1
的LIS序列的最小末尾,而minEnd[]
是递增的(简单的反证法就可以知道这一点),那我们可以在minEnd[]
中二分搜索array[i]
,就可以找到最长的那个可扩展序列的长度,这个长度再加1就是dp[i]
了,根据这个思路,代码如下:
首先一个辅助的二分查找函数,这个函数的解释参见我的第一篇博文:
private int binarySearch(int[] array, int n, int key) {
int first = 0, last = n;
while (first < last) {
int mid = first + (last - first) / 2;
if (array[mid] < key) {
first = mid + 1;
} else {
last = mid;
}
}
return first;
}
public int findLongestBetter(int[] array) {
// init
int n = array.length;
int[] dp = new int[n];
int[] minEnd = new int[n];
for (int i = 0; i < n; i++) {
minEnd[i] = Integer.MAX_VALUE;
}
// compute dp[i]
for (int i = 0; i < n; i++) {
// binary search expanded LIS and set dp[i]
int expandedLen = binarySearch(minEnd, i, array[i]) - 1 + 1; // which length should expand?
dp[i] = expandedLen + 1;
// don't forget to update minEnd[]
minEnd[expandedLen] = array[i];
}
// find max dp[i]
int lis = 1;
for (int i : dp) {
if (i > lis) lis = i;
}
return lis;
}
最优解法
你可能发现了,其实dp[]
现在已经没用了,我们只需要minEnd[]
就可以了,只要返回minEnd[]
中不是Integer.MAX_VALUE
的最后一个位置就可以了,我们可以聪明一点,记录更新的minEnd[]
的最后一个位置,这样可以避免对minEnd[]
再进行一次搜索:
public int findLongestBest(int[] array) {
// init
int n = array.length;
int[] minEnd = new int[n];
for (int i = 0; i < n; i++) {
minEnd[i] = Integer.MAX_VALUE;
}
int end = 0;
// compute dp[i]
for (int i = 0; i < n; i++) {
// binary search expanded LIS
int expandedLen = binarySearch(minEnd, i, array[i]); // which length should expand?
// update minEnd[]
minEnd[expandedLen] = array[i];
// set end
end = expandedLen > end ? expandedLen : end;
}
return end + 1;
}
参考资料
https://www.nowcoder.com/questionTerminal/585d46a1447b4064b749f08c2ab9ce66