博文声明:仅供本人学习交流使用,相关代码和资料已留下引用出处。
子序列定义:它是由原字符串在不改变字符的相对顺序
的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
【参考:【你的衣服我扒了 - 《最长公共子序列》】动态规划 - 不相交的线 - 力扣(LeetCode)】
一般这种求解 两个数组或者字符串 求最大或者最小 的题目都可以考虑动态规划,并且通常都定义
dp[i][j] 为 以 A[i], B[j] 结尾的 xxx
718. 最长重复子数组
1143.最长公共子序列 longest Common Subsequence LCS
1035. 不相交的线
【参考:516. 最长回文子序列 - 力扣(LeetCode)】
【参考:序列相关 DP 总结 - 知乎】
回文就是从左到右遍历得到的序列与从右到左遍历得到的序列相同的字符串
因此,我们要找到回文子序列,可以通过求原字符串与其反转字符串的最长公共子序列的长度
只不过这个子序列一定是连续的字符串而已
class Solution {
public int longestPalindromeSubseq(String s) {
String t=new StringBuilder(s).reverse().toString();
int m=s.length();
int n=t.length();
char[] sc=s.toCharArray();
char[] tc=t.toCharArray();
int[][] dp=new int[m+1][n+1];
// base case
// dp[i][0]=dp[0][j]=0
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
if(sc[i-1]==tc[j-1]) // 注意下标
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
return dp[m][n];
}
}
【参考:115. 不同的子序列 - 力扣(LeetCode)】
【参考:序列相关 DP 总结 - 知乎】
待理解
class Solution {
public int numDistinct(String s, String t) {
int m=s.length();
int n=t.length();
// m>=n
char[] sc=s.toCharArray();
char[] tc=t.toCharArray();
int[][] dp=new int[m+1][n+1];
// base case
// dp[0][j]=0
for(int i=0;i<=m;i++)
dp[i][0]=1; // 空串也是子串
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++){
if(sc[i-1]==tc[j-1]) // 注意下标
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
else
dp[i][j]=dp[i-1][j];
}
return dp[m][n];
}
}
【参考:522. 最长特殊序列 II - 力扣(LeetCode)】
题目翻译:给出一个字符串数组,在里面找出字符串满足当前字符串不是字符串数组中其他字符串的子序列,返回满足条件的字符串中 最长的字符串的长度
class Solution {
public int findLUSlength(String[] strs) {
int ans = -1;
int n = strs.length;
// 两两比较
for (int i = 0 ; i < n ; i++) {
if (strs[i].length() < ans) continue; // 题目求最长的
boolean flag = true;
for (int j = 0 ; j < n ; j++) {
if (i == j) continue; // 不和自己比较
//判断strs[i]是否为strs[j]的子序列
if (checkLCS(strs[i].toCharArray(), strs[j].toCharArray())) {
flag = false;
}
}
if (flag) {
ans = Math.max(ans, strs[i].length()); // 以前面的字符串为基准
}
}
return ans;
}
// 判断strs[i]是否为strs[j]的子序列
boolean checkLCS(char[] cs1, char[] cs2) {
int n = cs1.length, m = cs2.length;
if (n > m) return false;
int[][] dp = new int[n + 1][m + 1];
for (int i = 1 ; i <= n ; i++) {
for (int j = 1 ; j <= m ; j++) {
if (cs1[i - 1] == cs2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[n][m] == n; // 以前面的字符串为基准
}
}
class Solution {
public int findLUSlength(String[] strs) {
int n = strs.length;
int ans = -1;
for (int i = 0; i < n; ++i) {
boolean check = true;
for (int j = 0; j < n; ++j) {
if (i != j && isSubseq(strs[i], strs[j])) {
check = false;
break;
}
}
if (check) {
ans = Math.max(ans, strs[i].length());
}
}
return ans;
}
// 双指针 判断s是否是t的子序列
public boolean isSubseq(String s, String t) {
if (s.length() > t.length()) return false;
int ptS = 0, ptT = 0;
while (ptS < s.length() && ptT < t.length()) {
if (s.charAt(ptS) == t.charAt(ptT)) {
++ptS;
++ptT;
}else{
++ptT;
}
}
return ptS == s.length();
}
}
【参考:1218. 最长定差子序列 - 力扣(LeetCode)】
模拟过程 【参考:【彤哥来刷题啦】动态规划 & 接近双百! - 最长定差子序列 - 力扣(LeetCode)】
dp[x] 表示以 x 结尾的最长等差子序列的长度;
【参考:简单的dp - 最长定差子序列 - 力扣(LeetCode)】
dp[i]来记录以数字 i
为结尾的最长等差子序列的长度
class Solution {
public int longestSubsequence(int[] arr, int difference) {
int res=1;
Map<Integer,Integer> map=new HashMap<>();
for(int i:arr){
// 查找上一个数字为 i-difference 结尾的最长等差子序列的长度
int temp=map.getOrDefault(i-difference,0)+1;
map.put(i,temp);
res=Math.max(res,temp);
}
return res;
}
}
class Solution:
def longestSubsequence(self, arr: List[int], difference: int) -> int:
mydict=dict()
res=1
for x in arr:
temp=mydict.get(x-difference,0)+1
mydict[x]=temp
res=max(res,temp)
return res
【参考:300. 最长递增子序列 - 力扣(LeetCode)】
二分查找法
class Solution {
public int lengthOfLIS(int[] nums) {
int len = 0; // 二分查找数组的长度
int n = nums.length;
int[] top = new int[n];
for (int i = 0; i < n; i++) {
// 要处理的扑克牌
int target = nums[i];
/* 二分查找左侧边界 start */
int left = 0, right = len-1;
// 二分查找寻找最左侧的插入位置
while (left <= right) {
int mid = (left + right) / 2;
if (top[mid] >= target)
right = mid-1;
else
left = mid + 1;
}
if (left == len ) len++; // 检查出界情况
/* 二分查找左侧边界 end */
// left 就是插入位置
top[left] = target; // 把这张牌放到牌堆顶
}
return len; // 牌堆数就是 LIS 长度
}
}
二维上升子序列
【参考:354. 俄罗斯套娃信封问题 - 力扣(LeetCode)】
方法一:变成一维上升子序列,然后使用dp
【参考:俄罗斯套娃信封问题 - 俄罗斯套娃信封问题 - 力扣(LeetCode)】
class Solution {
public int maxEnvelopes(int[][] envelopes) {
if (envelopes.length == 0) {
return 0;
}
int n = envelopes.length;
Arrays.sort(envelopes, new Comparator<int[]>() {
public int compare(int[] e1, int[] e2) {
if (e1[0] != e2[0]) {
return e1[0] - e2[0];
} else {
return e2[1] - e1[1];
}
}
});
// 一维上升子序列
int[] f = new int[n];
Arrays.fill(f, 1);
int ans = 1;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (envelopes[j][1] < envelopes[i][1]) {
f[i] = Math.max(f[i], f[j] + 1);
}
}
ans = Math.max(ans, f[i]);
}
return ans;
}
}
【参考:动态规划设计:最长递增子序列 :: labuladong的算法小抄】
方法二:变成一维上升子序列,然后使用二分查找
class Solution {
public int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length;
// 按宽度升序排列,如果宽度一样,则按高度降序排列
Arrays.sort(envelopes, new Comparator<int[]>()
{
public int compare(int[] a, int[] b) {
return a[0] == b[0] ?
b[1] - a[1] : a[0] - b[0];
}
});
// 对高度数组寻找 LIS
int[] height = new int[n];
for (int i = 0; i < n; i++)
height[i] = envelopes[i][1];
return lengthOfLIS(height);
}
/* 返回 nums 中 LIS 的长度 */
public int lengthOfLIS(int[] nums) {
int len = 0; // 二分查找数组的长度
int n = nums.length;
int[] top = new int[n];
for (int i = 0; i < n; i++) {
// 要处理的扑克牌
int target = nums[i];
/* 二分查找左侧边界 start */
int left = 0, right = len-1;
// 二分查找寻找最左侧的插入位置
while (left <= right) {
int mid = (left + right) / 2;
if (top[mid] >= target)
right = mid-1;
else
left = mid + 1;
}
if (left == len ) len++; // 检查出界情况
/* 二分查找左侧边界 end */
// left 就是插入位置
top[left] = target; // 把这张牌放到牌堆顶
}
return len; // 牌堆数就是 LIS 长度
}
}
// 详细解析参见:
// https://labuladong.github.io/article/?qno=354