子序列不连续
-
最长递增子序列(300)
给一个整数数组
nums
,找到其中最长严格递增子序列的长度。动态规划
class Solution { public int lengthOfLIS(int[] nums) { // dp[i]表示以nums[i]结尾的最长递增子序列的长度 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(dp[i], res); } } return res; } }
贪心+二分查找
public class Solution { public int lengthOfLIS(int[] nums) { int len = nums.length; if (len <= 1) { return len; } // tail[i]表示长度为 i + 1 的上升子序列的末尾值 int[] tail = new int[len]; // 遍历第1个数,直接放在有序数组 tail 的开头 tail[0] = nums[0]; // end 表示有序数组 tail 的最后一个已经赋值元素的索引 int end = 0; for (int i = 1; i < len; i++) { int left = 0; int right = end + 1; // 二分法查找当前元素在上升子序列中应该插入的位置 while (left < right) { int mid = left + (right - left) / 2; if (tail[mid] < nums[i]) { left = mid + 1; } else { right = mid; } } // 当left = right 时循环结束,当前元素插入该位置 tail[left] = nums[i]; // 如果当前元素添加在子序列的末尾,即子序列长度+1,则更新end if (left == end + 1) { end++; } } // 返回子序列长度 return end + 1; } }
-
最长公共子序列(1143)
给定两个字符串
text1
和text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回0
。动态规划
// 二维dp class Solution { public int longestCommonSubsequence(String text1, String text2) { int m = text1.length(); int n = text2.length(); // dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { // text1[i] 与 text2[j]相同,则dp[i+1][j+1] = dp[i][j] + 1; if (text1.charAt(i) == text2.charAt(j)) { dp[i + 1][j + 1] = dp[i][j] + 1; // text1[i] 与 text2[j]不同,则看text1[0, i]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j]的最长公共子序列。 } else { dp[i + 1][j + 1] = Math.max(dp[i + 1][j], dp[i][j + 1]); } } } return dp[m][n]; } } // 一维dp class Solution { public int longestCommonSubsequence(String text1, String text2) { int m = text1.length(); int n = text2.length(); // dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] int[] dp = new int[n + 1]; // dp[i][j] <!=> dp[j] <=> dp[i + 1][j] for (int i = 0; i < m; i++) { // pre相当于dp[i][j] int pre = dp[0]; for (int j = 0; j < n; j++) { int cur = dp[j + 1]; if (text1.charAt(i) == text2.charAt(j)) { // 此时的pre = dp[j] = dp[i][j] dp[j + 1] = pre + 1; } else { // dp[j] 相当于 dp[i + 1][j] // dp[j + 1] 相当于 dp[i][j + 1] dp[j + 1] = Math.max(dp[j], dp[j + 1]); } pre = cur; } } return dp[n]; } }
-
不相交的线(1143)
在两条独立的水平线上按给定的顺序写下
nums1
和nums2
中的整数,可以绘制一些连接两个数字nums1[i]
和nums2[j]
的直线,这些直线需要同时满足满足:nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
其实就是求最长公共子序列!代码同上
子序列连续
-
最长连续递增序列(674)
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
动态规划
class Solution { public int findLengthOfLCIS(int[] nums) { int res = 1; int[] dp = new int[nums.length]; Arrays.fill(dp, 1); for (int i = 1; i <nums.length; i++) { if (nums[i] > nums[i - 1]) { dp[i] = dp[i - 1] + 1; } res = Math.max(res, dp[i]); } return res; } }
贪心
class Solution { public int findLengthOfLCIS(int[] nums) { int res = 1; int count = 1; for (int i = 1; i <nums.length; i++) { if (nums[i] > nums[i - 1]) { count++; } else { count = 1; } res = Math.max(res, count); } return res; } }
-
最长重复子数组(718)
给两个整数数组
nums1
和nums2
,返回两个数组中公共的 、长度最长的子数组的长度。动态规划
// 二维dp class Solution { public int findLength(int[] nums1, int[] nums2) { int m = nums1.length; int n = nums2.length; // dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 int[][] dp = new int[m + 1][n + 1]; int res = 0; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (nums1[i - 1] == nums2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } res = Math.max(res, dp[i][j]); } } return res; } } // 一维dp class Solution { public int findLength(int[] nums1, int[] nums2) { int m = nums1.length; int n = nums2.length; int[] dp = new int[n + 1]; int res = 0; for (int i = 1; i <= m; i++) { // 倒序遍历,防止覆盖 for (int j = n; j > 0; j--) { if (nums1[i - 1] == nums2[j - 1]) { dp[j] = dp[j - 1] + 1; } else { // 不同时要赋0 dp[j] = 0; } res = Math.max(res, dp[j]); } } return res; } }
滑动窗口
class Solution { public int findLength(int[] nums1, int[] nums2) { int m = nums1.length; int n = nums2.length; int res = 0; for (int i = 0; i < m; i++) { int len = Math.min(n, m - i); int maxlen = maxLength(nums1, nums2, i, 0, len); res = Math.max(res, maxlen); } for (int i = 0; i < n; i++) { int len = Math.min(m, n - i); int maxlen = maxLength(nums1, nums2, 0, i, len); res = Math.max(res, maxlen); } return res; } public int maxLength(int[] A, int[] B, int addA, int addB, int len) { int ret = 0, k = 0; for (int i = 0; i < len; i++) { if (A[addA + i] == B[addB + i]) { k++; } else { k = 0; } ret = Math.max(ret, k); } return ret; } }
-
最大子数组和(53)
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。动态规划
class Solution { public int maxSubArray(int[] nums) { // dp[i]:包括下标i的最大连续子序列和为dp[i] int[] dp = new int[nums.length]; dp[0] = nums[0]; int res = nums[0]; for (int i = 1; i < nums.length; i++) { dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); res = Math.max(res, dp[i]); } return res; } }
贪心
class Solution { public int maxSubArray(int[] nums) { int pre = nums[0]; int res = nums[0]; for (int i = 1; i < nums.length; i++) { pre = Math.max(pre + nums[i], nums[i]); res = Math.max(res, pre); } return res; } }
分治
public class Solution { public int maxSubArray(int[] nums) { int len = nums.length; if (len == 0) { return 0; } return maxSubArraySum(nums, 0, len - 1); } // 查询序列 nums [left, right] 区间内的最大子数组和 private int maxSubArraySum(int[] nums, int left, int right) { if (left == right) { return nums[left]; } int mid = left + (right - left) / 2; // 最大值在左区间、右区间、跨越左右区间出现 return max3(maxSubArraySum(nums, left, mid), maxSubArraySum(nums, mid + 1, right), maxCrossingSum(nums, left, mid, right)); } private int maxCrossingSum(int[] nums, int left, int mid, int right) { // 一定会包含 nums[mid] 这个元素 int sum = 0; int leftSum = Integer.MIN_VALUE; // 左半边包含 nums[mid] 元素,最多可以到什么地方 // 计算以 mid 结尾的最大的子数组的和 for (int i = mid; i >= left; i--) { sum += nums[i]; leftSum = Math.max(leftSum, sum); } sum = 0; int rightSum = Integer.MIN_VALUE; // 右半边不包含 nums[mid] 元素,最多可以到什么地方 // 计算以 mid+1 开始的最大的子数组的和 for (int i = mid + 1; i <= right; i++) { sum += nums[i]; rightSum = Math.max(rightSum, sum); } return leftSum + rightSum; } private int max3(int num1, int num2, int num3) { return Math.max(num1, Math.max(num2, num3)); } }
编辑距离
-
判断子序列(392)
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
动态规划
// 二维dp class Solution { public boolean isSubsequence(String s, String t) { int m = s.length(); int n = t.length(); // dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j] int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (s.charAt(i - 1) == t.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = dp[i][j - 1]; } } } return dp[m][n] == m ? true : false; } } // 一维dp class Solution { public boolean isSubsequence(String s, String t) { int m = s.length(); int n = t.length(); // dp[j] 表示字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度 int[] dp = new int[n + 1]; for (int i = 1; i <= m; i++) { int pre = dp[0]; for (int j = 1; j <= n; j++) { int cur = dp[j]; if (s.charAt(i - 1) == t.charAt(j - 1)) { dp[j] = pre + 1; } else { dp[j] = dp[j - 1]; } pre = cur; } } return dp[n] == m ? true : false; } }
双指针
class Solution { public boolean isSubsequence(String s, String t) { int m = s.length(); int n = t.length(); int i = 0, j = 0; while (i < m && j < n) { if (s.charAt(i) == t.charAt(j)) { i++; } j++; } return i == m; } }
-
不同的子序列(115)
给定一个字符串
s
和一个字符串t
,计算在s
的子序列中t
出现的个数。动态规划
// 二维dp class Solution { public int numDistinct(String s, String t) { // dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j] int[][] dp = new int[s.length() + 1][t.length() + 1]; // 初始化dp数组 // 空字符串是任意字符串的子串 for (int i = 0; i < s.length(); i++) { dp[i][0] = 1; } // dp for (int i = 1; i <= s.length(); i++) { for (int j = 1; j <= t.length(); j++) { if (s.charAt(i - 1) == t.charAt(j - 1)) { // 用s[i - 1]来匹配,即dp[i - 1][j - 1] // 不用s[i - 1]来匹配,即dp[i - 1][j] dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; } else { dp[i][j] = dp[i - 1][j]; } } } return dp[s.length()][t.length()]; } } // 一维dp class Solution { public int numDistinct(String s, String t) { int[] dp = new int[t.length() + 1]; // 初始化dp数组 // 空字符串是任意字符串的子串 dp[0] = 1; // dp for (int i = 1; i <= s.length(); i++) { int tmp1 = dp[0]; for (int j = 1; j <= t.length(); j++) { int tmp2 = dp[j]; if (s.charAt(i - 1) == t.charAt(j - 1)) { dp[j] = tmp1 + dp[j]; } else { dp[j] = dp[j]; } tmp1 = tmp2; } } return dp[t.length()]; } }
递归+记忆化搜索
class Solution { // 用于存储已经计算过的子问题的结果 int[][] memo; public int numDistinct(String s, String t) { memo = new int[s.length()][t.length()]; for (int i = 0; i < s.length(); i++) { for (int j = 0; j < t.length(); j++) { memo[i][j] = -1; } } return dfs(s, t, s.length() - 1, t.length() - 1); } public int dfs(String s, String t, int i, int j){ // 字符串t遍历结束,完成匹配 if (j < 0) { return 1; } // 字符串s遍历结束,未完成匹配 if (i < 0) { return 0; } // 已经计算过的直接返回结果 if (memo[i][j] != -1) { return memo[i][j]; } if (s.charAt(i) == t.charAt(j)) { memo[i][j] = dfs(s, t, i - 1, j - 1) + dfs(s, t, i - 1, j); } else { memo[i][j] = dfs(s, t, i - 1, j); } return memo[i][j]; } }
-
两个字符的删除操作(583)
给定两个单词
word1
和word2
,返回使得word1
和word2
相同所需的最小步数。动态规划
class Solution { public int minDistance(String word1, String word2) { int len1 = word1.length(); int len2 = word2.length(); // dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,要达到相等,需要删除元素的最少次数。 int[][] dp = new int[len1 + 1][len2 + 1]; // 初始化dp for (int i = 0; i <= len1; i++) { dp[i][0] = i; } for (int i = 0; i <= len2; i++) { dp[0][i] = i; } // dp for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { // 情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1 // 情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1 // 情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2 dp[i][j] = Math.min(dp[i - 1][j - 1] + 2, Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)); } } } return dp[len1][len2]; } }
长度相加 - 最大公共子序列长度*2
class Solution { public int minDistance(String word1, String word2) { int len1 = word1.length(); int len2 = word2.length(); // dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2 的最长公共子序列。 int[][] dp = new int[len1 + 1][len2 + 1]; // dp for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { if (word1.charAt(i - 1) == word2.charAt(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 len1 + len2 - dp[len1][len2] * 2; } }
-
编辑距离(72)
给你两个单词
word1
和word2
, 请返回将word1
转换成word2
所使用的最少操作数 。可以使用插入、删除、替换
动态规划
class Solution { public int minDistance(String word1, String word2) { int len1 = word1.length(); int len2 = word2.length(); // dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j] int[][] dp = new int[len1 + 1][len2 + 1]; // 初始化dp for (int i = 0; i <= len1; i++) { dp[i][0] = i; } for (int i = 0; i <= len2; i++) { dp[0][i] = i; } // dp for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { // 若相同则无需操作 dp[i][j] = dp[i - 1][j - 1]; } else { // 若不同,则需要增、删、改 // 操作1:word1删除/word2增加 dp[i][j] = dp[i - 1][j] + 1 // 操作2:word2删除/word1增加 dp[i][j] = dp[i][j - 1] + 1 // 操作3:改元素 dp[i][j] = dp[i - 1][j - 1] + 1 dp[i][j] = Math.min(dp[i - 1][j - 1] + 1, Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)); } } } return dp[len1][len2]; } }
回文
-
回文子串(647)
给你一个字符串
s
,请你统计并返回这个字符串中 回文子串 的数目。动态规划
class Solution { public int countSubstrings(String s) { int len = s.length(); int res = 0; if (s == null || len < 1) return 0; //dp[i][j]:s字符串下标i到下标j的字串是否是一个回文串,即s[i, j] boolean[][] dp = new boolean[len][len]; for (int j = 0; j < len; j++) { for (int i = 0; i <= j; i++) { //当两端字母一样时,才可以两端收缩进一步判断 if (s.charAt(i) == s.charAt(j)) { //i--,j++,即两端收缩之后i,j指针指向同一个字符或者i超过j了,必然是一个回文串 if (j - i <= 2) { dp[i][j] = true; } else { //否则通过收缩之后的字串判断 dp[i][j] = dp[i + 1][j - 1]; } } //两端字符不一样,不是回文串 else { dp[i][j] = false; } } } //遍历每一个字串,统计回文串个数 for (int i = 0; i < len; i++) { for (int j = 0; j < len; j++) { if (dp[i][j]) res++; } } return res; } }
中心扩展法
// 如果回文长度是奇数,那么回文中心是一个字符;如果回文长度是偶数,那么中心是两个字符。 class Solution { public int countSubstrings(String s) { int len = s.length(); // 单个字符都是回文 int res = len; for (int i = 0; i < len; i++) { // 以s[i]为中心 int left = i - 1; int right = i + 1; while (left >= 0 && right < len && s.charAt(left--) == s.charAt(right++)) { res++; } // 以s[i],s[i+1]为中心 left = i; right = i + 1; while (left >= 0 && right < len && s.charAt(left--) == s.charAt(right++)) { res++; } } return res; } }
-
最长回文子序列(516)
给你一个字符串
s
,找出其中最长的回文子序列,并返回该序列的长度。动态规划
// 二维dp class Solution { public int longestPalindromeSubseq(String s) { int len = s.length(); char[] c = s.toCharArray(); // dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j] int[][] dp = new int[len][len]; // 遍历i的时候要从下到上遍历,保证下一行的数据是经过计算的 for (int i = len - 1; i >= 0; i--) { // 单个字符位置都是回文 dp[i][i] = 1; for (int j = i + 1; j < len; j++) { if (c[i] == c[j]) { // 如果s[i]与s[j]相同,同时加入,那么dp[i][j] = dp[i + 1][j - 1] + 2; dp[i][j] = dp[i + 1][j - 1] + 2; } else { // 如果s[i]与s[j]不相同,分别加入s[i]、s[j]看哪一个可以组成最长的回文子序列 dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); } } } return dp[0][len - 1]; } } // 一维dp class Solution { public int longestPalindromeSubseq(String s) { int len = s.length(); char[] c = s.toCharArray(); // dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j] int[] dp = new int[len]; // 从下到上、从左到右遍历 for (int i = len - 1; i >= 0; i--) { dp[i] = 1; int pre = 0; for (int j = i + 1; j < len; j++) { int tmp = dp[j]; if (c[i] == c[j]) { // pre = dp[i - 1][j + 1] dp[j] = pre + 2; } else { // dp[j]在赋新值之前存储的值对应dp[i + 1][j] dp[j] = Math.max(dp[j], dp[j - 1]); } pre = tmp; } } return dp[len - 1]; } }