最长递增子序列
300. 最长上升子序列(Medium)
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n^2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
一开始没有考虑清楚,以为可以使用循环遍历的方式来解答,但是做了之后才发现不可行,因为遍历过程中当前选择的元素会影响后面的选择,比如[10,9,2,5,3,4] 输出是2,结果却是3,遍历:2->5,正确结果:2->3->4。
解法一:递归,寻找最长递增子序列的时候,每个位置上的数字有两个选择:选或不选,前面的数字会影响到后面能选择的数字(因为是递增的),这种方法时间复杂度为O(2^n),空间复杂度为O(n^2)。
class Solution{
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0){
return 0;
}
return lengthOfLIS(nums, Integer.MIN_VALUE, 0);
}
private int lengthOfLIS(int[] nums, int pre, int cur) {
//base case
if (cur == nums.length){
return 0;
}
//包含当前元素
int taken = 0;
//如果当前数大于pre,taken++
if (nums[cur] > pre){
taken = 1 + lengthOfLIS(nums, nums[cur], cur + 1);
}
//不包含
int nottaken = lengthOfLIS(nums, pre, cur + 1);
return Math.max(taken, nottaken);
}
}
解法二:将暴力递归转换为动态规划,再明确动态规划是简化暴力递归的方法,优化掉重复计算的部分。老方法:
- 第一步找变量:cur和result(最长递增子序列)
- 第二步由递归找出转移方程:dp[i]=Math.max(dp[i],dp[j]+1),dp[i]表示前i个数的最大递增子序列数量
- 转移方程的意义是指在j属于[0,i],新增i,如果nums[i]>nums[j],则dp[i]=dp[j]+1,否则不变
public class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0){
return 0;
}
int[] dp = new int[nums.length];
int res = 1;
//填充dp,最短子序列是1
Arrays.fill(dp, 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;
}
}
解法三:贪心思想+二分查找。
- 基本的思想是:如果前面的数越小,后面接上一个随机数,就会有更大的可能性构成一个更长的“上升子序列”。具体可见LeetCode
- 准备一个数组tail存储遍历到的最长子序列的尾,初始化为空
- 遍历nums,如果遇到nums[i]>tail最后一个元素,那么直接添加到其后面
- 如果nums[i]<tail最后一个元素,在有序数组 tail 中查找第 1 个等于大于 num 的那个数,试图让它变小;
- 如果有序数组 tail 中存在等于 num 的元素,什么都不做,因为以 num 结尾的最短的“上升子序列”已经存在;
- 如果有序数组 tail 中存在大于 num 的元素,找到第 1 个,让它变小,这样我们就找到了一个“结尾更小”的“相同长度”的上升子序列。
public class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length
if (len <= 1){
return len;
}
//定义tail:长度为 i + 1 的上升子序列的末尾最小是几
int[] tail = new int[len];
tail[0] = nums[0];
//end表示tail最后一个已赋值索引
int end = 0;
for (int i = 1; i < len; i++) {
if (nums[i] > tail[end]){
end++;
tail[end] = nums[i];
} else {
//用二分查找找到第一个>=nums[i]的元素
int l = 0;
int r = end;
while (l < r){
int mid = l + (r - l) / 2;
if (tail[mid] < nums[i]){
l = mid + 1;
} else {
r = mid;
}
}
tail[l] = nums[i];
}
}
end++;
return end;
}
}
646. 最长数对链(Medium)
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
示例 :
输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]
注意:
给出数对的个数在 [1, 1000] 范围内。
解法一:
- 这道题和上道题求最长上升子序列是一样的思想,但是这道题是不用保持原位置的,而上道题是要求数字之间的相对位置不能变。
- 所以先对数组按照结束大小排序(跟排课的那道题目类似),接着进行暴力递归:每个数对都有两种选择,包括进或者不包括。
- 不出意料,暴力递归解法超时了。
class Solution {
public static int findLongestChain(int[][] pairs) {
if (pairs == null || pairs.length == 0){
return 0;
}
//对数组进行排序:按尾部大小进行排序
Arrays.sort(pairs, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1];
}
});
return findLongestChain(pairs, Integer.MIN_VALUE, 0);
}
private static int findLongestChain(int[][] pairs, int pre, int cur) {
if (cur == pairs.length){
return 0;
}
//包含当前数对
int taken = 0;
if (pairs[cur][0] > pre){
taken = 1 + findLongestChain(pairs, pairs[cur][1], cur + 1);
}
//不包括
int nottaken = findLongestChain(pairs, pre, cur + 1);
return Math.max(taken, nottaken);
}
}
解法二:
- 将暴力递归转为动态规划,避免暴力递归中的大量重复计算
- 首先,先找变量:标记到第几个数组的变量cur和结果result,接着找转移方程:dp[i]代表前i(从0开始计数)个数形成的最长数对的状态转移方程:dp[i]=Math.max(dp[i-1],dp[j]+1),遍历到dp[i]时,遍历0~i-1,如果满足条件的就执行转移方程。
- 转移方程就是从前一个推到后一个的方程,所以找的时候要关注这个推导过程
public int findLongestChain(int[][] pairs) {
if (pairs == null || pairs.length == 0) {
return 0;
}
//排序
Arrays.sort(pairs, (a, b) -> (a[1] - b[1]));
int n = pairs.length;
int[] dp = new int[n];
int res = 0;
//初始化dp,最短是1
Arrays.fill(dp, 1);
//填充dp数组
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
//往前寻找的时候,如果发现满足条件的就比较 dp[i], dp[j] + 1 谁更大
if (pairs[j][1] < pairs[i][0]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
解法三:贪心,这道题和排课那道题思想是一致的,所以可以使用同样的方法解答:先对数组按照尾部进行排序,从第一个数对开始,依次判断后面的数对,能否与前面的数对组成数对链,能组的话长度就+1,这样找到的数对链就是最优解。
public int findLongestChain(int[][] pairs) {
if (pairs == null || pairs.length == 0) {
return 0;
}
//排序
Arrays.sort(pairs, (a, b) -> (a[1] - b[1]));
int n = pairs.length;
int res = 1;
int edn = pairs[0][1]
for(int i=1;i<len;i++){
if(pairs[i][0]>end){
end=pairs[i][1];
res++;
}
}
return res;
}
376. 摆动序列(Medium)
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如,[1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3)是正负交替出现的。相反, [1,4,7,2,5]和[1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。
示例 2:
输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
示例 3:
输入: [1,2,3,4,5,6,7,8,9]
输出: 2
解法一:
- 先分析题意:连续数字之间的差在正负之间交替,且数字相对位置不变的序列,成为摆动序列。
- 每个数字有可能成为上升元素或者下降元素,这次找的是上升,下个就是下降,反之亦然
- 最后比较两个大小,取大的返回
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length < 2){
return nums.length;
}
return 1 + Math.max(wiggleMaxLength(nums, true, 0), wiggleMaxLength(nums, false, 0));
}
private int wiggleMaxLength(int[] nums, boolean isUp, int cur) {
//当前数作为摆动序列
int maxCount = 0;
for (int i = cur + 1; i < nums.length; i++) {
if (isUp && nums[i] > nums[cur] || (!isUp && nums[i] < nums[cur])){
maxCount = Math.max(maxCount, 1 + wiggleMaxLength(nums, !isUp, i));
}
}
return maxCount;
}
}
解法二:
- 转为动态规划,因为一个元素可以既可以作为上升元素,也可以作为下降元素,所以数组一共有两种:dp[]和down[],up[i] 存的是目前为止最长的以第i个元素结尾的上升摆动序列的长度,down[i]也类似。
- 变量是i和result,由于上升和下降是交替的,所以找到将第i个元素作为上升摆动序列的尾部的时候就更新 up[i]
- 现在我们考虑如何更新 up[i],我们需要考虑前面所有的降序结尾摆动序列,也就是找到 down[j],满足 j < i 且 nums[i] >nums[j]。类似的, down[i]也会被更新。
public class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length < 2){
return nums.length;
}
int[] up = new int[nums.length];
int[] down = new int[nums.length];
//遍历数组
for (int i = 1; i < nums.length; i++) {
for(int j = 0; j < i; j++) {
//找到一个上升元素就与下降元素相比
if (nums[i] > nums[j]) {
up[i] = Math.max(up[i],down[j] + 1);
} else if (nums[i] < nums[j]) {
//反之同理
down[i] = Math.max(down[i],up[j] + 1);
}
}
}
return 1 + Math.max(down[nums.length - 1], up[nums.length - 1]);
}
}
解法三:
线性动态规划,具体过程参考leetcode官方解答: 数组中的任何元素都对应下面三种可能状态中的一种:
- 上升的位置,意味着 nums[i] > nums[i - 1]
- 下降的位置,意味着 nums[i] < nums[i - 1]
- 相同的位置,意味着 nums[i] == nums[i - 1]
更新的过程如下:
-
如果 nums[i] > nums[i-1],意味着这里在摆动上升,前一个数字肯定处于下降的位置。所以 up[i] = down[i-1] + 1, down[i]与 down[i-1]保持相同。
-
如果 nums[i] < nums[i-1],意味着这里在摆动下降,前一个数字肯定处于下降的位置。所以 down[i] = up[i-1] + 1, up[i]与 up[i-1]保持不变。
-
如果 nums[i] == nums[i-1] ,意味着这个元素不会改变任何东西因为它没有摆动。所以 down[i]与 up[i] 与 down[i-1]和 up[i-1]都分别保持不变。
最后,我们可以将 up[length-1]和 down[length-1]中的较大值作为问题的答案,其中 lengthlength 是给定数组中的元素数目。
public class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length < 2){
return nums.length;
}
int[] up = new int[nums.length];
int[] down = new int[nums.length];
up[0] = down[0] = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] > nums[i - 1]) {
up[i] = down[i - 1] + 1;
down[i] = down[i - 1];
} else if (nums[i] < nums[i - 1]) {
down[i] = up[i - 1] + 1;
up[i] = up[i - 1];
} else {
down[i] = down[i - 1];
up[i] = up[i - 1];
}
}
return Math.max(down[nums.length - 1], up[nums.length - 1]);
}
}
解法四:贪心,具体解法和第三种线性动态规划一致,不同的是优化了空间,不再使用数组,而是用变量记录大小。
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int up = 1;
int down = 1;
for (int i = 1; i < nums.length; i++) {
//差值
int c = nums[i] - nums[i - 1];
//差值为正,就说明比down大一
if (c > 0) {
up = down + 1;
} else if (c < 0) {
//反之同理
down = up + 1;
}
}
return Math.max(up, down);
}
}
最长公共子序列
对于两个子序列S1和S2,找出它们的最长公共子序列。(参考CyC2018大佬)
可以定义一个二维数组dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
- 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 综上,最长公共子序列的状态转移方程为:

对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
与最长递增子序列相比,最长公共子序列有以下不同点:
- 针对的是两个序列,求它们的最长公共子序列。 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;
- 在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。
- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
1143. 最长公共子序列(Medium)
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。
解法一:暴力递归,求两个字符串的最长公共字符串,分别以i和j开头递归这两个字符串,如果遇到S1.charAt(i)==S2.charAt(j)就++,否则i++之后的子序列长和j++之后的子序列长比较,取大的返回
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
if (text1 == null || text2 == null){
return 0;
}
return longestCommonSubsequence(text1, text2, 0, 0);
}
private int longestCommonSubsequence(String text1, String text2, int i, int j) {
//base case
if (i == text1.length() || j == text2.length()){
return 0;
}
//如果S1.charAt(i)==S2.charAt(j),长度加1
if (text1.charAt(i) == text2.charAt(j)){
return 1 + longestCommonSubsequence(text1, text2, i + 1, j + 1);
}else {
//如果不想等,取大的返回
return Math.max(longestCommonSubsequence(text1, text2, i + 1, j), longestCommonSubsequence(text1, text2, i, j + 1));
}
}
}
解法二:改为动态规划,老套路。
- 找变量:i和j以及res
- 转移方程:dp[i][j]=dp[i-1][j-1]+1 如果S1.charAt(i)==S2.charAt(j)
- dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) 如果不等
public int longestCommonSubsequence(String text1, String text2) {
char[] s1 = text1.toCharArray();
char[] s2 = text2.toCharArray();
int[][] dp = new int[s1.length + 1][s2.length + 1];
for(int i = 1 ; i < s1.length + 1 ; i ++){
for(int j = 1 ; j < s2.length + 1 ; j ++){
//如果末端相同
if(s1[i - 1] == s2[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[s1.length][s2.length];
}