文章目录
- 动态规划(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] 结尾的最长子序列长度。
转移方程:
初始状态: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 递增的最少操作次数
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] 之间匹配得到的想要的结果。
- 状态定义
定义 dp[i][j] 表示 text1[0:i] 和 text2[0:j] 的最长公共子序列。 - 状态转移方程
- 状态的初始化
- 遍历方向与范围
由于 dp[i + 1][j + 1] 依赖与 dp[i][j] , dp[i + 1][j], dp[i][j + 1],所以 i 和 j 的遍历顺序是从小到大的。 - 最终返回结果
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 的走完
}
}