动态规划&二分查找-最长上升子序列(及变型)★

题目来源:https://leetcode-cn.com/problems/longest-increasing-subsequence/

1. 给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:


    可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
    你算法的时间复杂度应该为 O(n2) 。


进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

解法一:动态规划

  • 初始化状态数组为1,dp[i]表示前i个数字中,最长上升子序列的最大长度。(初始化为1是因为单个数字必定是上升子序列)
  • 状态转移方程:对于i∈[0,n),对于j∈[0,i),即nums[j]是nums[i]左边的项,如果nums[j]<nums[i],那么上升子序列长度在dp[j]的基础上加一,最长上升子序列长度为max(dp[i], dp[j]+1),因为j有很多个,更新的dp[j]+1不一定大于前面更新的dp[i]。否则什么也不做。
class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n = len(nums)
        if n <= 1:
            return n
        dp = [1 for i 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)

解法二:二分查找

方法一最后的搜索过程复杂度为O(n^2),外层O(n)是动不了的,可以考虑对内层循环优化,让内层复杂度变为O(logn)。

对于nums[i](i∈[0,len(nums))),如果nums[i]大于tails最大的元素,则直接加到tails尾部。否则,从tails中找到第一个比nums[i]大的元素,用nums[i]替换掉它。tails是一个最长上升子序列。

class Solution:
    def lengthOfLIS(self, nums):
        n = len(nums)
        if n <= 1:
            return n
        
        tails = [nums[0]]
        for i in range(n):        # 外层循环复杂度依然为O(n)
            if nums[i] > tails[-1]:          # nums[i]大于tails最大值,则直接加到尾部
                tails.append(nums[i])
                continue                     # 没必要做下面的工作,所以结束本次循环,开始下一次
            
            low = 0                          # 二分查找tails中比nums[i]大的项的最小索引
            high = len(tails) - 1
            while low < high:
                mid = low + (high - low) // 2
                if nums[i] > tails[mid]:
                    low = mid + 1
                else:
                    high = mid
            tails[low] = nums[i]             # 替换为nums[i]
        return len(tails)

2. 给定一个无序的整数数组,输出其中一组最长上升子序列的的下标集合。

LIC的变型,先计算最长上升子序列的长度,记为max_len,从max_len开始每次递减1,并取相应的下标。

class Solution(object):
    def findLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n = len(nums)
        if n == 0:
            return 0
        dp = [1 for _ in range(n)]
        res = []
        for i in range(n):
            for j in range(i):
                if nums[j] < nums[i]:
                    dp[i] = max(dp[i], dp[j] + 1)
        max_dp = max(dp)
        while max_dp:     
            res.append(dp.index(max_dp))    # 取下标
            max_dp -= 1
        return([res::-1])

3. 给定一个未排序的整数数组,找到最长递增子序列的个数。

来源:https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/

额外维护一个数组counts,记录以每个整数结尾的最长上升子序列的个数。

class Solution(object):
    def findNumberOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n = len(nums)
        if n == 0:
            return 0
        dp = [1 for _ in range(n)]
        counts = [1 for _ in range(n)]
        cnt = 0
        for i in range(n):
            for j in range(i):
                if nums[j] < nums[i]:
                    if dp[j] >= dp[i]:      # 正常情况下,应有dp[j] <= dp[i],如果不满足则说明dp[i]还没更新好,更新dp[i]
                        dp[i] = dp[j] + 1
                        counts[i] = counts[j]    # 相当于初始化
                    elif dp[j] + 1 == dp[i]:      # 如果dp[i]之前有连续相等的dp[j],那么从第二个开始,都会有dp[i] == dp[j] + 1,而counts[j]是相互独立的(因为分别以nums[j]结尾),所以每次都要加上counts[j]
                        counts[i] += counts[j]
        max_len = max(dp)
        for i in range(n):
            if dp[i] == max_len:     # 最长上升子序列可能有多个
                cnt += counts[i]
        return cnt

 

参考:

https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-e/

https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/solution/dong-tai-gui-hua-jian-li-shuang-zhong-yi-wei-dpbia/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值