动态规划—最长上升子序列问题(LIS)
1. 子序列和子串的区别
- 子序列(subsequene):子序列并不要求连续,例如:序列[4, 6, 5]是[1, 2, 4, 3, 7, 6, 5]的一个子序列;
- 子串(substring、subarray):子串一定是原始字符串的连续子串。
2. 最长上升子序列 (可不连续)
题目
方法1、暴力解法
可以首先计算出数组的所有子序列,时间复杂度度为\(O(2^N)\),再对子串依次判定是否为递增,时间复杂度为o(n),所以总的时间复杂度为\(O(N.2^N)\)。
方法2、动态规划
1.定义状态
dp[i]表示以nums[i]为结尾的最长上升子序列的长度。
2.状态转移方程
只要nums[i]严格大于在它位置之前的某个数,则nums[i]就可以接在该数后面形成一个更长的一个上升子序列。
3.初始化
dp[i]=1,1个字符初始都为长度为1的上升子序列。
4. 输出
状态数组dp的最大值才是整个数组的最长上升子序列的长度。
5.时间复杂度和空间复杂度
时间复杂度:\(O(N^2)\),首先要遍历数组中每一个数i,复杂度为N,然后需要判定i之前的dp[j]的最大,j最糟情况复杂度也为N,因此总的复杂度为\(o(N^2)\)。
空间复杂度度:\(O(N)\),需要维护一个状态数组dp。
def lengthoflis(nums):
if not nums:
return 0
n = len(nums)
dp = [1 for _ in range(n)]
for i in range(1,n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i],dp[j]+1)
return max(dp)
方法3、贪心+二分查找
一个简单的贪心算法,如果我们想要上升子序列尽可能的长,则我们希望让序列上升的尽可能慢,也就是每次在上升子序列最后加上的那个数尽可能的小。
1.定义状态
d[i] 表示长度为 i+1 的最长上升子序列的末尾元素的最小值。
证明 d[i]关于i是单调递增的。
因为如果d[j]>d[i]且j<i,我们从长度为i的最长上升子序列中删除j-i个元素,使得序列长度与j一致。由于该序列严格上升所以d[j]<d[i],则与假设矛盾,因此d的单调性得证。
2.初始化
用len记录目前最长上升子序列的长度,起始时len为1,i=0,则d[0] = num[0]
3. 算法流程
依次遍历数组nums的每个元素,并更新数组d和len的值。
- 如果nums[i] > d[len] ,则直接加入d数组末尾,并更新len = len+1
- 否则,在d数组中二分查找,找到第一个比nums[i]小的数d[k],并更新d[k+1] = nums[i].
4.输出
有序数组d的长度,就是所求的最长上升子序列的长度
5.复杂度分析
时间复杂度度:O(NlogN),遍历数组使用了O(N),二分查找法使用了O(logn)。
空间复杂度:O(N),要维护状态数组d。
def lengthoflis(nums):
if not nums:
return 0
d[0] = num[0]
for i in range(1,len(nums)):
if i>d[-1]:
d.append(i)
else:
l, r = 0, len(d)-1
loc = r
while l <=r:
mid = (l+r)//2
if d[mid] >= i:
loc = mid
r = mid -1
else:
l = mid + 1
d[loc] = n
return len(d)
3. 最长上升子串
方法1、暴力遍历
从数组第一数开始遍历,求以该数为初始值的最长递增序列。 时间复杂度度为\(O(N^2)\)。
方法2、动态规划
1. 定义状态
dp[i]表示以nums[i]为结尾的最长上升子序列的长度。
2.初始化
dp = dp[1]* n ,初始化都为1
3. 状态转移方程
4.输出
状态数组dp的最大值才是整个数组的最长上升子序列的长度。
5.复杂度分析
时间复杂度:\(O(N)\),遍历数组中每一个数 。
空间复杂度度:\(O(N)\),需要维护一个状态数组dp。
def lengthoflis(nums):
if not nums:
return 0
n = len(nums)
dp = [1 for _ in range(n)]
for i in range(1,n):
if nums[i] > nums[i-1]:
dp[i] = dp[i-1]+1
return max(dp)