[Leetcode]——Longest increasing subsequence
题目描述
Given an unsorted array of integers, find the length of longest increasing subsequence.
Example:
Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
Note:
There may be more than one LIS combination, it is only necessary for you to return the length.
Your algorithm should run in O(n2) complexity.
Follow up: Could you improve it to O(n log n) time complexity?
解题思路
这是经典的最长上升子序列的问题,花了一天半的时间在考虑这个问题,当然也解决了[华为–合唱队]的问题。
这题一共有两种思路,一种是动态规划的思想,参考动态规划:最长上升子序列的解释,将其中的例子拿过来,如下:
让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。
前1个数 d(1)=1 子序列为2;
前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7
前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1
前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5
前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6
前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4
前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3
前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8
前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9
d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5
总结一下,d(i)就是找以A[i]结尾的,在A[i]之前的最长上升子序列+1(如果A[i]大于这个最长上升子序列的话),如果A[i]之前的数都大于A[i],则d[i]=1,表示由A[i]独自构成一个序列,按照这个思路写了代码,并且在LeetCode上已经编译通过了,如下所示(请注意对异常情况要进行判断,否则提交不通过):
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if len(nums) == 0 or nums is None:
return 0
dp = [1] * len(nums)
result = 1
for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j]+1)
result = max(result, dp[i])
return result
上述的算法复杂度为O(n^2),在LeetCode上跑起来时间需要1140ms,如何降低复杂度呢?同样参考动态规划:最长上升子序列的第二个例子:
我们再举一个例子:有以下序列A[]=3 1 2 6 4 5 10 7,求LIS长度。
我们定义一个B[i]来储存可能的排序序列,len为LIS长度。我们依次把A[i]有序地放进B[i]里。(为了方便,i的范围就从1~n表示第i个数)
A[1]=3,把3放进B[1],此时B[1]=3,此时len=1,最小末尾是3
A[2]=1,因为1比3小,所以可以把B[1]中的3替换为1,此时B[1]=1,此时len=1,最小末尾是1
A[3]=2,2大于1,就把2放进B[2]=2,此时B[]={1,2},len=2
同理,A[4]=6,把6放进B[3]=6,B[]={1,2,6},len=3
A[5]=4,4在2和6之间,比6小,可以把B[3]替换为4,B[]={1,2,4},len=3
A[6]=5,B[4]=5,B[]={1,2,4,5},len=4
A[7]=10,B[5]=10,B[]={1,2,4,5,10},len=5
A[8]=7,7在5和10之间,比10小,可以把B[5]替换为7,B[]={1,2,4,5,7},len=5
其实这里用到了贪心的思想,即对于一个上升子序列,显然当前最后一个元素越小,越有利于添加新的元素,这样LIS长度自然更长。但是上述讲解存在一个错误(看了很多blog上都是这么写的,不知道为什么大家都没有发现这个错误??),就是不应该对B数组进行替换操作,而应该将新的数插入到B数组中,并记录下此时对应的最长子序列,举个例子来说:
A = [1, 3, 6, 7, 9, 4, 10, 5, 6]
按照参考的blog所讲述的方法,判断出来最终的B=[1, 3, 4, 5, 6],长度为5,这是因为当判断到元素"4"时,由于4比{6, 7, 9}都要小,因此要删除{6, 7, 9},再插入4,但这种方法是错误的,显然最终的B=[1, 3, 6, 7, 9, 10],长度为6。
语言上可能难以说清楚这个来龙去脉,还是看代码吧:
class Solution3:
def find_pos(self, dp, elem): #用二分法查找nums[i]应该插入的位置,算法复杂度为O(logn)
left, right = 0, len(dp) - 1
while left <= right:
mid = (left + right) // 2
if dp[mid] > elem:
right = mid - 1
elif dp[mid] < elem:
left = mid + 1
else:
return mid
return right + 1
def lengthOfLIS(self, nums: List[int]) -> int:
if len(nums) == 0 or nums is None:
return 0
dp = [float('inf')] * len(nums)
dp[0] = nums[0]
res = []
res.append(1)
for i in range(1, len(nums)):
if nums[i] > dp[-1]:
dp.append(nums[i])
res.append(len(dp))
else:
pos = self.find_pos(dp, nums[i]) # 找到nums[i]对应的位置,插入dp中
dp[pos] = nums[i]
res.append(pos + 1) #同时记录下此时的位置,对应nums[i]之前的最长上升子序列
return max(res)
测试了一下在LeetCode上的时间是44ms,大大降低了复杂度,另外一个比较省事的做法是在python中引入bisect包,可以实现find_pos的功能:
import bisect
class Solution2:
def lengthOfLIS(self, nums: List[int]) -> int:
if len(nums) == 0 or nums is None:
return 0
dp = [float('inf')] * len(nums)
dp[0] = nums[0]
res = []
res.append(1)
for i in range(1, len(nums)):
pos = bisect.bisect_left(dp, nums[i])
res.append(pos+1)
dp[pos] = nums[i]
return max(res)
至此这个问题算是彻底结束了,其中很多编程的细节都需要自己去体会,且该问题经常会出现在互联网公司中的笔试题中,且会有各种变形,希望能有所收获吧!