题目链接: https://leetcode-cn.com/problems/longest-increasing-subsequence/.
在Youtube,B站上搜了以下,关于O(nlogn)解法没有找到一讲我秒懂的tutorial,因此详细记录一下LIS 时间复杂度为O(nlogn)的做法。
解法1:DP,通过双层遍历,如果num[i] > num[j],dp[i] = max(dp[j]) + 1 (j from 0 to i)。
class Solution(object):
def lengthOfLIS(self, nums):
if not nums:
return 0
dp = [0] * len(nums)
dp[0] = 1
res = 1
for i in range(1, len(nums)):
tmp = 1
for j in range(i):
if nums[i] > nums[j]:
tmp = max(tmp, dp[j] + 1)
dp[i] = tmp
res = max(res, dp[i])
return res
但是DP解法是有优化空间:
举个例子num = [1, 2, 5, 3, 7],显然dp[2] = 3 (所对应的LIS为[1, 2, 5]),dp[3] = 3 (所对应的LIS为[1, 2, 3])。当i = 4时,由于num[2] < num[3] 并且dp[3] >= dp[2] (此处有贪心思想),所以dp[4]的时候不需要考虑dp[2]。
解法2:贪心 + 二分。
class Solution(object):
def lengthOfLIS(self, nums):
if not nums: return 0
tail = [0] * len(nums)
size = 0 # tail 实际长度
for num in nums:
i, j = 0, size
while i != j:
mid = (i + j) // 2
if tail[mid] < num: i = mid + 1
else: j = mid
tail[i] = num
if i == size: size += 1;
return size
tail[i]表示的是:nums在长度为i的时候,其LIS的最后元素的最小值。
当tail的有效size为m的时候,m就是我们的答案。(但是请注意, tail[:size]并不一定等同于LIS,仅仅是长度相同)
以nums = [1, 2, 5, 3, 7]为例:
tail[0]:长度为1 -> [1]; [2]; [5]; [3]; [7] -> 1
tail[1]:长度为2 -> [1,2]; [1,5]; [1,3] ,…, [3, 7] -> 2
tail[2]:长度为3 -> [1,2,5], [1,2,3] ,…, [2,5,7] -> 3
我们可以容易得出tail是单调递增的。原因:tail[0] = min(nums),而我们假设tail[1]的连续上升序列为(nums[i], nums[i+1]),显然nums[i] >= min(nums),nums[i+1] > nums[i]。所以tail[1]必然大于tail[0]。
当我们遍历nums的时候。
- 如果nums[i]大于tail[size],tail[size + 1] = nums[i]
- 如果nums[i]小于tail[size],我们利用二分查找,把大于nums[i]最少的tail[j]替换成nums[i]
这一步的原因是因为贪心,比如nums = [1,2,8,4]的时候,num = 8的时候, tail[2] = [1, 2, 8]。但是num =
4的时候,很明显[1,2,4]是要比[1,2,8]更好的。比如nums = [1,2,8,4,7]的时候,LIS是由[1,2,4] + [7]组成的。而由于之前的论证,tail是单调递增,所以我们可以用二分查找,完成替换操作。
时间复杂度分析:iterate the element of input (n) * binary search (logn) = O(nlogn)