文章目录

  • 动态规划(DP)
  • [★300. 最长递增子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)
  • [1964. 找出到每个位置为止最长的有效障碍赛跑路线](https://leetcode.cn/problems/find-the-longest-valid-obstacle-course-at-each-position/)
  • [673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/)
  • [354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/)
  • [1691. 堆叠长方体的最大高度](https://leetcode.cn/problems/maximum-height-by-stacking-cuboids/)
  • [2407. 最长递增子序列 II](https://leetcode.cn/problems/longest-increasing-subsequence-ii/)
  • 2111. 使数组 K 递增的最少操作次数
  • [14. 最长公共前缀](https://leetcode-cn.com/problems/longest-common-prefix/)
  • [718. 最长重复子数组](https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/)
  • 1143.最长公共子序列
  • [1035. 不相交的线](https://leetcode-cn.com/problems/uncrossed-lines/)
  • [521. 最长特殊序列 Ⅰ](https://leetcode.cn/problems/longest-uncommon-subsequence-i/)
  • [522. 最长特殊序列 II](https://leetcode.cn/problems/longest-uncommon-subsequence-ii/)


最长公共子序列(LCS,Longest Common Subsequence)


最长上升子序列(LIS,Longest Increasing Subsequence)


最长上升公共子序列 (LCIS ,Longest Common Increasing Subsequence)

动态规划(DP)

一、DP:
1、把一个大的问题分解成一个一个的子问题。
2、如果得到了这些子问题的解,然后经过一定的处理,就可以得到原问题的解。
3、如果这些子问题与原问题有着结构相同,即小问题还可以继续的分解。
4、这样一直把大的问题一直分下去,问题的规模不断地减小,直到子问题小到不能再小,最终会得到最小子问题。
5、最小子问题的解显而易见,这样递推回去,就可以得到原问题的解。

二、DP的具体实现:
1、分析问题,得到状态转换方程(递推方程)。
2、根据状态转换方程,从原子问题开始,不断的向上求解,知道得到原问题的解。
3、在通过递推方程不断求解的过程,实际上是一个填表的过程。

★300. 最长递增子序列

方法一:动态规划

定义 dp[i] 表示以 nums[i] 结尾的最长子序列长度。
转移方程: LCS LIS LICS_递增子序列
初始状态:dp = [1] * len(nums),每个元素可以单独成为子序列。
方向:从小到大计算 dp 数组的值。
返回值:返回 dp 列表最大值,即可得到全局最长上升子序列长度。

# Dynamic programming.
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums: return 0
        n = len(nums)
        dp = [1] * n
        for i in range(n):
            for j in range(i):
                if nums[j] < nums[i]: # 非严格递增为 '<=' 。
                    dp[i] = max(dp[i], dp[j] + 1)

        return max(dp)
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int res = 1;
        for (int i = 1; i < nums.length; i++){
            for (int j = 0; j < i; j++){
                if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

方法二:贪心 + 二分查找
如果要使上升子序列尽可能的长,则需要让序列上升得尽可能慢,因此希望每次在上升子序列最后加上的那个数尽可能的小。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # q, res = [0] * len(nums), 0
        # for num in nums:
        #     i, j = 0, res
        #     while i < j:
        #         m = (i + j) // 2
        #         if q[m] < num: i = m + 1 
        #         else: j = m
        #     q[i] = num 
        #     # 可能已经破坏了最长严格递增子序列,
        #     # 只有长度增加了才回复正常。不影响长度。
        #     if j == res: res += 1
        # return res
       
        q, lengh = [nums[0]], 1 # 维护一个长度变量比求 len(q) 快点
        for x in nums[1:]: 
            if x > q[-1]:
                q.append(x)
                lengh += 1
            else:
                idx = bisect.bisect_left(q,x)               
                q[idx] = x 

        return lengh
class Solution {
    public int lengthOfLIS(int[] nums) {
        int idx = 0, n = nums.length;
        int[] d = new int[n];
        d[idx] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > d[idx]) d[++idx] = nums[i];
            else {
                int j = 0, k = idx; 
                while (j <= k) {
                    int mid = (j + k) >> 1;
                    if (d[mid] < nums[i]) j = mid + 1;
                    else k = mid - 1;                    
                }
                d[j] = nums[i];
            }
        }
        return idx + 1;
    }
}

class Solution {
    public int lengthOfLIS(int[] nums) {
        List<Integer> lis = new ArrayList<>();
        lis.add(nums[0]);
        for (int i = 1; i < nums.length; i++){
            if (nums[i] > lis.get(lis.size() - 1)){
                lis.add(nums[i]);
            } else {
                int j = 0, k = lis.size();
                while (j < k){
                    int m = (j + k) / 2;
                    if (lis.get(m) < nums[i]) j = m + 1;
                    else k = m;
                }
                lis.set(j, nums[i]);
            }
        }
        return lis.size();
    }
}

1964. 找出到每个位置为止最长的有效障碍赛跑路线

class Solution:
    def longestObstacleCourseAtEachPosition(self, obstacles: List[int]) -> List[int]:
        d = list()
        ans = list()
        for x in obstacles:
            # 这里需要改成 >=
            if not d or x >= d[-1]:
                d.append(x)
                ans.append(len(d))
            else:
                # 如果是最长严格递增子序列,这里是 bisect_left
                # 如果是最长递增子序列,这里是 bisect_right
                loc = bisect_right(d, x)
                ans.append(loc + 1)
                d[loc] = x        
        return ans
class Solution {
    public int[] longestObstacleCourseAtEachPosition(int[] obstacles) {
        int n = obstacles.length;
        int[] ans = new int[n];
        List<Integer> list = new ArrayList();
        for (int i = 0; i < n; i++) {
            int x = obstacles[i];
            int m = list.size();
            if (m == 0 || x >= list.get(m - 1)) {
                list.add(x);
                ans[i] = m + 1;
            } else {
                int j = binSearch(list, x);
                ans[i] = j + 1;
                list.set(j, x);
            }
        }
        return ans;
    }

    private int binSearch(List<Integer> list, int target) {
        int l = 0, r = list.size();
        while (l < r) {
            int mid = l + r >> 1;
            if (list.get(mid) <= target) l = mid + 1;
            else r = mid;
        }
        return l;
    }
}

673. 最长递增子序列的个数

354. 俄罗斯套娃信封问题

先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序。之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案。

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        n = len(envelopes)
        envelopes.sort(key=lambda x: (x[0], -x[1]))
        f = [envelopes[0][1]]
        for i in range(1, n):
            if (x := envelopes[i][1]) > f[-1]:
                f.append(x)
            else:
                index = bisect.bisect_left(f, x)
                f[index] = x
        
        return len(f)
class Solution {
    public int maxEnvelopes(int[][] envelopes) {
        int n = envelopes.length;
        // 按宽度升序排列,如果宽度一样,则按高度降序排列
        Arrays.sort(envelopes, (a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);        
        int[] height = new int[n]; // 对高度数组寻找 LIS
        for (int i = 0; i < n; i++)
            height[i] = envelopes[i][1];
        return lengthOfLIS(height);
    }
    /* 返回 nums 中 LIS 的长度 */
    public int lengthOfLIS(int[] nums) {
        int piles = 0, n = nums.length;
        int[] top = new int[n];
        for (int i = 0; i < n; i++) {            
            int poker = nums[i]; // 要处理的扑克牌
            int left = 0, right = piles;
            // 二分查找插入位置
            while (left < right) {
                int mid = (left + right) / 2;
                if (top[mid] >= poker) right = mid;
                else left = mid + 1;
            }
            if (left == piles) piles++;            
            top[left] = poker; // 把这张牌放到牌堆顶
        }       
        return piles;  // 牌堆数就是 LIS 长度
    }
}

1691. 堆叠长方体的最大高度

class Solution:
    def maxHeight(self, cuboids: List[List[int]]) -> int:
        n, ans = len(cuboids), 0
        for c in cuboids: c.sort()
        cuboids.sort()
        f = [0] * n        
        for i, (_, w, h) in enumerate(cuboids):
            for j, (_, a, b) in enumerate(cuboids[:i]):
                if a <= w and b <= h: 
                    f[i] = max(f[i], f[j]) # i 接 j
            f[i] += h
            ans = max(ans, f[i])
        return ans
class Solution {
    public int maxHeight(int[][] cuboids) {
        for (int[] c : cuboids)
            Arrays.sort(c);
        Arrays.sort(cuboids, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] != b[1] ? a[1] - b[1] : a[2] - b[2]);
        int ans = 0, n = cuboids.length;
        int[] f = new int[n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j)
                // 排序后,cuboids[j][0] <= cuboids[i][0]
                if (cuboids[j][1] <= cuboids[i][1] && cuboids[j][2] <= cuboids[i][2])
                    f[i] = Math.max(f[i], f[j]); // j 可以堆在 i 上
            f[i] += cuboids[i][2];
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}

2407. 最长递增子序列 II

class Solution {
    public int lengthOfLIS(int[] nums, int k) {
        // 单点更新,区间查询
        int ans = 0;
        for (int x:nums) {
            // 查询区间 [x - k, x - 1] 的最大值
            int cnt = query(root, 0, N, Math.max(0, x - k), x - 1);
            update(root, 0, N, x, ++cnt);
            ans = Math.max(ans, cnt);
        }
        return ans;
    }

    class Node {        
        Node left, right;        
        int val;
    }
    private int N = (int) 1e5;
    private Node root = new Node();
    void update(Node node, int start, int end, int x, int val) {
        if (start == end) {
            node.val = val;
            return ;
        }
        pushDown(node);
        int mid = (start + end) >> 1;
        if (x <= mid) update(node.left, start, mid, x, val);
        else update(node.right, mid + 1, end, x, val);
        pushUp(node);
    }
    public int query(Node node, int start, int end, int l, int r) {
        if (l <= start && end <= r) return node.val;
        pushDown(node);
        int mid = start + end >> 1, ans = 0;
        if (l <= mid) ans = query(node.left, start, mid, l, r);
        if (r > mid) ans = Math.max(ans, query(node.right, mid + 1, end, l, r));
        return ans;
    }
    private void pushUp(Node node) {
        node.val = Math.max(node.left.val, node.right.val);
    }
    private void pushDown(Node node) {
        if (node.left == null) node.left = new Node();
        if (node.right == null) node.right = new Node();
    }
}

2111. 使数组 K 递增的最少操作次数

Leetcode

from bisect import bisect_right

class Solution:
    def kIncreasing(self, arr: List[int], k: int) -> int:
        # 最长上升子序列
        # def LIS(nums: List[int]) -> int:
        #     a = []
        #     for x in nums:
        #         i = bisect.bisect_right(a, x) # left 严格递增,right 非严格递增         
        #         if i == len(a): a.append(x)
        #         else: a[i] = x
        #     return len(a) 

        def LIS(arr: List[int]) -> int:
            q = [arr[0]]
            # 使用 bisect_right,可以取相等(非严格递增)
            for num in arr[1:]:
                if num >= q[-1]: q.append(num)
                else:
                    index = bisect_right(q, num)
                    q[index] = num
                    
            return len(q)

        return len(arr) - sum(LIS(arr[start::k]) for start in range(k))
class Solution {
    public int kIncreasing(int[] arr, int k) {
        int res = arr.length;
        for (int i = 0; i < k; i++){
            List<Integer> a = new ArrayList<>();
            for (int j = i; j < arr.length; j += k) a.add(arr[j]);
            res -= LIS(a);
        }
        return res;
    }

    public int LIS(List<Integer> nums){
        List<Integer> a = new ArrayList<>();
        for (int num : nums){
            int i = 0, j = a.size();
            while (i < j){
                int m = (i + j ) >> 1;
                if (a.get(m) > num) j = m;
                else i = m + 1;
            }
            if (i == a.size()) a.add(num);
            else a.set(i, num);
        }
        return a.size();
    }
}

14. 最长公共前缀

排序,求第一个和最后一个单词的最长公共前缀。

class Solution:
    def longestCommonPrefix(self, strs: List[str]) -> str:  
        strs.sort()
        a, b = strs[0], strs[-1]
        for i in range(min(len(a), len(b))):
            if a[i] != b[i]: return a[:i]        
        return a # a 是 b 的前缀子串
class Solution {
    public String longestCommonPrefix(String[] strs) {
        Arrays.sort(strs);
        String s = strs[0], t = strs[strs.length - 1];
        int n = Math.min(s.length(), t.length());
        for (int i = 0; i < n; i++){
            if (s.charAt(i) != t.charAt(i)) return s.substring(0, i);
        }
        return s; // strs.length = 1
    }
}

718. 最长重复子数组

计算两个数组的最长公共子数组。

class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
    	# 方法一:暴力
        m, n = len(nums1), len(nums2)
        mx = 0        
        for i in range(m):
            for j in range(n):
                if nums1[i] == nums2[j]: # A[i:] 与 B[j:] 最长公共前缀
                    k = 1
                    x = min(m - i, n - j)
                    while k < x:
                        if nums1[i + k] != nums2[j + k]: break
                        k += 1
                    mx = max(mx, k)                        
        return mx

动态规划: dp[i][j] 表示 A[i:] 和 B[j:] 的最长公共前缀,答案即为所有 dp[i][j] 中的最大值。如果 A[i] == B[j],那么 dp[i][j] = dp[i - 1][j - 1] + 1,否则 dp[i][j] = 0。

考虑到这里 dp[i][j] 的值从 dp[i + 1][j + 1] 转移得到,所以我们需要倒过来,首先计算 dp[len(A) - 1][len(B) - 1],最后计算 dp[0][0]。

class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        n, m = len(nums2), len(nums1)
        # dp = [[0] * (n + 1) for _ in range(m + 1)]
        dp, ans = [0] * (n + 1), 0
        for i in range(m):
            # # 逆序遍历可降维,正序需要滚动数组
            # for j in range(n-1,-1,-1):
            #     if nums1[i] == nums2[j]:
            #         dp[j+1] = dp[j] + 1
            #         ans = max(ans, dp[j+1])
            #     else:dp[j+1] = 0
            tmp = [0] * (n + 1) # 滚动数组
            for j in range(n):
                if nums1[i] == nums2[j]:
                    # dp[i+1][j+1] = dp[i][j] + 1
                    # ans = max(ans, dp[i+1][j+1])
                    tmp[j+1] = dp[j] + 1
                    ans = max(ans, tmp[j+1])
            dp = tmp
        return ans
class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        # 方法三:滑动窗口
        def maxLength(i: int, j: int, length: int) -> int:
            ret = x = 0
            for k in range(length):
                if nums1[i + k] == nums2[j + k]:
                    x += 1
                    ret = max(ret, x)
                else: x = 0
            return ret
        
        m, n = len(nums1), len(nums2)
        ret = 0
        for i in range(m):
            length = min(n, m - i)
            ret = max(ret, maxLength(i, 0, length))
        for i in range(n):
            length = min(m, n - i)
            ret = max(ret, maxLength(0, i, length))
        return ret
class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        # 方法四:二分查找 + 哈希
        base, mod = 113, 10**9 + 9

        def check(length: int) -> bool:
            hashA = 0
            for i in range(length):
                hashA = (hashA * base + nums1[i]) % mod
            bucketA = {hashA}
            mult = pow(base, length - 1, mod)
            for i in range(length, len(nums1)):
                hashA = ((hashA - nums1[i - length] * mult) * base + nums1[i]) % mod
                bucketA.add(hashA)
            
            hashB = 0
            for i in range(length):
                hashB = (hashB * base + nums2[i]) % mod
            if hashB in bucketA:
                return True
            for i in range(length, len(nums2)):
                hashB = ((hashB - nums2[i - length] * mult) * base + nums2[i]) % mod
                if hashB in bucketA:
                    return True

            return False

        left, right = 0, min(len(nums1), len(nums2))
        ans = 0
        while left <= right:
            mid = (left + right) // 2
            if check(mid):
                ans = mid
                left = mid + 1
            else:
                right = mid - 1
        return ans

1143.最长公共子序列

动态规划:求两个数组或者字符串的最长公共子序列问题。

子序列可以是不连续的;子数组(子字符串)需要是连续的。
单个数组或者字符串要用动态规划时,可以把动态规划 dp[i] 定义为 nums[0:i] 中想要求的结果;当两个数组或者字符串要用动态规划时,可以把动态规划定义成两维的 dp[i][j] ,其含义是在 A[0:i] 与 B[0:j] 之间匹配得到的想要的结果。

  1. 状态定义
    定义 dp[i][j] 表示 text1[0:i] 和 text2[0:j] 的最长公共子序列。
  2. 状态转移方程
    LCS LIS LICS_递增子序列_02
  3. 状态的初始化
    LCS LIS LICS_递增子序列_03
  4. 遍历方向与范围
    由于 dp[i + 1][j + 1] 依赖与 dp[i][j] , dp[i + 1][j], dp[i][j + 1],所以 i 和 j 的遍历顺序是从小到大的。
  5. 最终返回结果
    LCS LIS LICS_leetcode_04
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        m, n = len(text1), len(text2)

        # dp = [[0] * (n + 1) for _ in range(m + 1)]
        # for i in range(m):
        #     for j in range(n):
        #         if text1[i] == text2[j]:
        #             dp[i + 1][j + 1] = dp[i][j] + 1
        #         else:
        #             dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j])
        # return dp[m][n]
      
        if text1 in text2: return m # 剪枝
        if text2 in text1: return n 

        dp = [0] * (n + 1) # 降维 需要维护 上一个左边的元素
        for i in range(m):
            upLeft = dp[0]
            for j in range(n):
                tmp = dp[j+1]
                if text1[i] == text2[j]: dp[j+1] = upLeft + 1
                else: dp[j+1] = max(dp[j],dp[j+1])
                    
                upLeft = tmp                
        
        return dp[n]

1035. 不相交的线

k 条互不相交的直线分别连接了数组 nums1 和 nums2 的 k 对相等的元素,而且这 k 对相等的元素在两个数组中的相对顺序是一致的,因此,这 k 对相等的元素组成的序列即为数组 nums1 和 nums2 的公共子序列。计算可以绘制的最大连线数,即为计算数组数组 nums1 和 nums2 的最长公共子序列的长度。
最长公共子序列问题是典型的二维动态规划问题。

class Solution:
    def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
        m, n = len(nums1), len(nums2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i, x in enumerate(nums1):
            for j, y in enumerate(nums2):
                if x == y:
                    dp[i + 1][j + 1] = dp[i][j] + 1
                else:
                    dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j])
        
        return dp[m][n]

521. 最长特殊序列 Ⅰ

class Solution {
    public int findLUSlength(String a, String b) {
        int m = a.length(), n = b.length();
        if(m != n) return Math.max(m, n);
        return a.equals(b) ? -1 : m;
    }
}

522. 最长特殊序列 II

对于给定的某个字符串 str[i],如果它的一个子序列 sub 是「特殊序列」,那么 str[i] 本身也是一个「特殊序列」。

class Solution {
    public int findLUSlength(String[] strs) {
        int n = strs.length, max = -1;        
        for(int i = 0; i < n; i++){
            boolean flag = true;
            for(int j = 0; j < n; j++){
                if(i == j) continue;
                if(check(strs[i], strs[j])) {
                    flag = false;
                    break;
                }
            }
            if(flag) max = Math.max(max, strs[i].length());
        }
        return max;
    }

    public boolean check(String s, String t) {
        int m = s.length(), n = t.length();
        int i = 0, j = 0;
        // 判断 s 是 t 的子序列 
        while(i < m && j < n){
            if(s.charAt(i) == t.charAt(j)) i++;
            j++;
        }        
        return i == m; // s 的走完
    }
}