[python刷题模板] 最长递增子序列LIS、最长公共子序列 LCS

[python刷题模板] 最长递增子序列LIS、最长公共子序列 LCS

    • 一、 算法&数据结构
      • 1. 描述
      • 2. 复杂度分析
      • 3. 常见应用
      • 4. 常用优化
    • 二、 模板代码
      • 1. 二分优化LIS
      • 2. 二分优化LCS
      • 3. 二分优化LCS,s1去重
      • 4.最长非降子序列\无序序列转换为递增序列
    • 三、其他
    • 四、更多例题
    • 五、参考链接

一、 算法&数据结构

1. 描述

LIS和LCS都是动态规划典型题目,常规做法是O(n^2)的,但是可以优化成O(nlgn)。

2. 复杂度分析

  1. LIS, O(nlog2n)
  2. LCS,O(nlog2n)

3. 常见应用

  1. LIS
    • 单调子序列的最少划分
    • 最长单调子序列的字典序最小解
    • K 长单调子序列
    • 无序序列转换为递增序列
  2. LCS
    • 带权最长公共子序列
    • 路径回溯
    • 公共子序列方案数
    • 最长公共递增子序列

4. 常用优化

  1. 对LIS来说,可以转换DP思路,dp[i]表示长度为i的子序列,能够维护的最小尾巴值。这样dp就是递增的,可以二分。
  2. 对于LCS来说,可以通过下标转化为LIS,实现二分。

二、 模板代码

1. 二分优化LIS

例题: 300. 最长递增子序列

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:

        def lis(nums):
            n = len(nums)
            # dp = [float('inf')] * (n+1)  # dp[i](从1开始) 为长度为i的上升子序列的可以的最小尾巴  
            dp = []          
            for num in nums:
                if not dp or num > dp[-1]:
                    dp.append(num)
                else:
                    pos = bisect_left(dp,num)
                    dp[pos] = num            
            return len(dp)

        return lis(nums)

2. 二分优化LCS

链接: 1143. 最长公共子序列
lcs_n2 是O(n2)的,lcs_nlgn是优化的。
优化思路,把s1的数全部转化为下标,逆序拼接,在s2中映射对应下标,然后计算LIS。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        def lis_nlgn(nums):
            n = len(nums)
            # dp = [float('inf')] * (n+1)  # dp[i] 为长度为i的上升子序列的可以的最小尾巴  
            dp = []          
            for num in nums:
                if not dp or num > dp[-1]:
                    dp.append(num)
                else:
                    pos = bisect_left(dp,num)
                    dp[pos] = num            
            return len(dp)

        def lcs_nlgn(s1,s2):
            s1_poses = collections.defaultdict(list)
            for i in range(len(s1)-1,-1,-1):  # 需要逆序
                s1_poses[s1[i]].append(i)
            cleaned_s2_pos = list(itertools.chain.from_iterable([s1_poses[x] for x in s2 if x in s1_poses]))
            same_lis = lis_nlgn(cleaned_s2_pos)
            return same_lis


        def lcs_n2(s1,s2):
            m,n = len(s1),len(s2)
            dp = [[0]*(n+1) for _ in range(m+1)]
            for i in range(1,m+1):
                for j in range(1,n+1):
                    if s1[i-1] == s2[j-1]:
                        dp[i][j] = dp[i-1][j-1] + 1
                    else:
                        dp[i][j] = max(dp[i-1][j],dp[i][j-1])
            return dp[m][n]
        
        return lcs_nlgn(text1,text2)

3. 二分优化LCS,s1去重

链接: 1300. 转变数组后最接近目标值的数组和

和模板2的区别在于,s1这个字符串本身是去重的,那么转化下标时,每个数的下标不需要list,效率快不少。

class Solution:
    def minOperations(self, target: List[int], arr: List[int]) -> int:
        '''
        题意一看是找LCS(最长公共子序列)。然后给arr中补上target中剩下的数就行。
        但是LCS标准DP是O(nm),过不了。
        需要转化成LIS(最长上升子序列)来二分优化。
        本题最长公共子序列的意思是target中的数,在arr中尽量找到的多,且相对位置保持不变。
        位置就是下标,位置有序转换过去就是下标递增。
        由于target本身是去重的,因此可以翻转下标,用下标唯一来代表这个数。
        也就是说把target中每个数的下标映射到arr中,寻找子序列,下标是递增的,问题转化为LIS
        arr中的不在target中的字符是没用的,可以先干掉。
        '''        
        
        def lis_nlgn(nums):
            # 最长上升序列的二分优化版本
            n = len(nums)
            # dp = [float('inf')] * (n+1)  # dp[i] 为长度为i的上升子序列的可以的最小尾巴  
            dp = []          
            for num in nums:
                if not dp or num > dp[-1]:
                    dp.append(num)
                else:
                    pos = bisect_left(dp,num)
                    dp[pos] = num            
            return len(dp)

        def lcs_nlgn_when_s1_unique(s1,s2):
            # 当s1本身是去重的,用这个,下标不用拼list
            s1_poses = {v:pos for pos,v in enumerate(target)}
            cleaned = [s1_poses[x] for x in arr if x in s1_poses]           
            return lis_nlgn(cleaned)
        def lcs_nlgn(s1,s2):
            # 转lis利用二进制优化。
            s1_poses = collections.defaultdict(list)
            for i in range(len(s1)-1,-1,-1):  # 需要逆序
                s1_poses[s1[i]].append(i)
            cleaned_s2_pos = list(itertools.chain.from_iterable([s1_poses[x] for x in s2 if x in s1_poses]))
            return lis_nlgn(cleaned_s2_pos)
             
        return len(target)-lcs_nlgn_when_s1_unique(target,arr)

4.最长非降子序列\无序序列转换为递增序列

链接: 926. 将字符串翻转到单调递增
这题最佳做法是DP,O(n)。但LIS也能做,从提交时间上来看差的不多。
参见: [LeetCode解题报告] 926. 将字符串翻转到单调递增
和LIS的区别,bisect_left改成bisect_right,dp append条件从>变成>=。

class Solution:
    def minFlipsMonoIncr(self, s: str) -> int:
        def lis_non_decrease(nums):
            n = len(nums)
            # dp = [float('inf')] * (n+1)  # dp[i] 为长度为i的上升子序列的可以的最小尾巴  
            dp = []          
            for num in nums:
                if not dp or num >= dp[-1]:
                    dp.append(num)
                else:
                    pos = bisect_right(dp,num)
                    dp[pos] = num            
            return len(dp)
        
        return len(s) - lis_non_decrease([int(c) for c in s])

三、其他

  1. 还可以有最长下降,最长非升。

四、更多例题

  • 待补充

五、参考链接

  • [LeetCode解题报告] 926. 将字符串翻转到单调递增

你可能感兴趣的:(python刷题模板,python,leetcode,动态规划)