各种子序列:
最长上升子序列(LIS):Longest Increasing Subsequence
最长连续序列(LCS):Longest Consecutive Sequence
最长连续递增序列(LCIS):Longest Continuous Increasing Subsequence
最长公共子序列(LCS):Longest Common Subsequence
leetcode题库:
300 动态规划解最长子序列子串等一类问题之最长上升子序列[Thorold's Deer]
673/674动态规划解最长子序列子串等一类问题之最长连续递增序列[Reindeer]
1312动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]
128/LC397动态规划解最长子序列子串等一类问题之最长连续子序列[White-lipped Deer]
1143动态规划解最长子序列子串等一类问题之最长公共子序列[Hog Deer]
动态规划解最长子序列子串等一类问题之最长上升子序列[Thorold's Deer]
子串与子序列区别:子串不可跳跃,子序列可以跳跃,如 “AC”是“ABCDEFG”的子序列,而不是子串。 而“ABC”则是其子串
定义状态:
dp[i]dp[i]表示在区间nums[0....i]区间范围内的最长上升子序列的长度
比较索引ii与其前面出现的某个位置jj的大小
当nums[i]<=nums[j],说明jj处的值要比i处的值的,要是形成子序列则是nums[0...j,i]这样的结果,这时候j到i不能形成递增,以i结尾的子序列所形成的最长子序列的长度等价于以j结尾的子序列所形成的最长子序列的长度,即dp[i]=dp[j]
当nums[i]>nums[j],说明jj处的值小于i处值,形成的子序列是nums[0...j,i]这样的结果,这时候从j到i是递增的,这时候需要在长度上+1,即dp[i]=dp[j+1]
取上述两种情况的max,动态转移方程为: dp[i] = Math.max(dp[i], dp[j] + 1)|0<=j
举例:如果遍历到ii位置,在[0-i] 区间内有[0-j] j
边界条件:
当nums[0...i]只有一个元素时,即以这一个元素结尾的子序列,最长上升子序列是其自身,为1
核心代码
public int lengthOfLIS(int[] nums) { //dp[i]: 到i为止 (对于所有 j in [0, i], 记录max length of increasing subsequence if (nums == null || nums.length == 0) { return 0; } int len = nums.length; int[] dp = new int[len]; int res = 0; for (int i = 0; i < len; i++) { dp[i] = 1; for (int j = 0; j < i; j++) { //i 位置的数与[0,i]位置之间的数比较,如果大于进逻辑 if (nums[i] > nums[j]) { //等于dp[i]或者dp[j] + 1(j对应的值比i小)的最大值 dp[i] = Math.max(dp[i], dp[j] + 1); } } res = Math.max(res, dp[i]); } return res; }
方法2:贪心+二分
准备一个辅助数组tails,其中tails[i]表示长度为i+1即nums[0...i]的序列尾部元素的值
辅助数组tails一定是严格单调递增的,即在nums[0...j..i]区间上,tails[j] 假设nums[0...j..i]这个区间上,tails[j]>=tails[i],从i这个子序列向前删除i-j个元素,这时候长度变为j的子序列,这时候的尾部元素的值为x 根据tails数组的定义,x 而x又是tails[j]的值,即x=tails[j] 得出tails[i]>tails[j],这与一开始假设的条件矛盾,假设条件不成立 题目要求最长连续递增序列,序列本身可以跳跃,但是题意要求连续,也即是不可跳跃 定义状态: dp[i]表示以i位置结尾,即nums[i]值结尾的,最长连续递增序列的长度 只要关注当前位置i与其前一个的位置i-1的值的大小: nums[i]>nums[i-1],i至少可以与i-1形成一个连续递增序列,因为它们俩挨着,并且是递增的,长度上是dp[i-1]+1 nums[i]<=nums[i-1],这时候,不能形成连续递增序列,后一个数要比前一个数小,呈下降的趋势,注意=不认为是递增的 方法1:DP(O(N)) 方法2:DP(O(1)) 因为只会依赖于i与i-1这两个状态,借助一个常数的级别的一维数组dp[2],辅以奇偶数的小技巧 定义状态: dp[i]表示以i位置结尾,即nums[i]值结尾的,最长连续递增序列的长度 dp初始化为1,因为nums[i]自身可以形成一个长度为1的最长递增序列 遍历[0...i],再套一层[0...j],其中j<i 当nums[j]<nums[i],说明以[...j,i]这段可以形成最长递增序列,长度是dp[j]+1,其中dp[j]是以j为结尾的最长递增序列的长度 当nums[j]≥nums[i],以[...j,i]是不能形成最长递增序列的,为dp[i],其被初始化为1了 接下来要统计最长递增序列的个数,准备长度n的数组counter,定义count[i]为以nums[i]结尾的最长递增子序列的组合数量,这其中有两个限定条件,一是以nums[i]结尾,二是最长递增子序列,不是最长的不是这个counter数组考虑的,举例,1,2,4,3,5,4,7,2,最长递增序列有1,2,4,5,7;1,2,3,5,7;1,2,3,4,7三种情况,以nums[6]=7结尾的counter[6]=3 下面是如何生成这个counter数组: 总体要满足nums[i] > nums[j],才有意义,这样可以形成递增序列 当dp[j] + 1>dp[i],说明第一次找到以nums[i]为结尾的最长递增子序列,长度为dp[j] + 1,进而可以推出counter[i] = counter[j], 以nums[i]结尾的最长递增子序列的组合数=以nums[j]结尾的最长递增子序列的组合数,这个可以这么理解:当[...j]形成的组合数是值的话,其每一种组合结尾补上[i],即[...j,i],对于组合数本身是没有增加的,还是A值,唯独只是递增子序列的长度+1了 当dp[j] + 1=dp[i],说明这个长度已经找到过一次了,counter[i] += counter[j],现在的组合方式+counter[j]的组合方式 复杂度分析 时间复杂度:O(N^2),两个for loop 空间复杂度:O(N),dp与counter数组长度N
源问题,如上图,让字符串成为回文串的最少插入次数
定义状态 n为str的长度,dp[n][n]是一张二维表 定义:dp[i][j]表示的是子串str[i][j]最少添加多少字符可以使其整体变成一个回文,注意,是最少 要求的结果是dp[0][n-1]表示的是str[0...n-1]也就是这个字符从开头到结尾,要想形成回文,最少的添加字符的个数 如何填写这张dp table呢? 分三大类讨论 1. str[i...j]只有一个字符,即i==j,如图,str[i...j]=A,一个字符要形成回文,很简单,不要向其增加字符,即其本身可以形成回文,即dp[i][j]=0 2. str[i...j]有两个字符 2.1,当str[i]==str[j],即两个字符相同时,其与场景1的情况一样,例如,AA不需要添加字符,即可使其成为回文,即dp[i][j]=0 2.2,当str[i]!=str[j],即两个字符不同时,例如,AB,可以在前面加一个B,变成BAB,也可以在后面加一个A,变成ABA,总之是只要添加一个字符,即可使其变成回文,即dp[i][j]=1 3. str[i...j]有三个或者三个以上字符,这就属于一般的情况了 3.1.当str[i]==str[j]时,例如,ABDCA,只需要考虑BDC,也即要求dp[i][j],转化为求dp[i+1][j-1],只要搞定str[i+1...j-1]部分,使其变成回文,再加上str[i]与str[j],整体就变成回文了,即转移方程:dp[i][j]=dp[i+1][j-1] 3.2.当str[i]!=str[j]时,有两种方法使其变成回文,一种是先让str[i...j-1]变成回文,在ii的左边添加str[j],整体就会变成回文,例如ABECD,可以先考虑ABEC这部分,一个方案是ABECEBA,再在ABECEBA左边加上str[j],即D,带上源字符的D,变成了DABECEBAD;另外一种方案,同理,先让str[i+1...j]变成回文,最后在右边加上str[i],最终变成了整体回文,那么,动态转移方程式什么呢? 要想使str[i...j-1]变成回文,其最少的添加次数是dp[i][j-1],再加上往左侧补的字符,最终应该是dp[i][j-1]+1 同理可得,dp[i+1][j]+1 dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1 生成dptable的代码,注意遍历时,综合考虑了以上分析的三种情况 下文中需要使用到这个buildDP,做个一个抽离 时间复杂度:O(N^2),N是字符的长度 空间复杂度:O(N^2),dp表的空间 给定一个字符串str,如果可以在str的任意位置添加字符,请返回在添加字符最少的情况下,让str整体都是回文字符串的结果。 这个问题比Step1的问题多了一步,要求返回一种使其变成整体回文的结果,例如,源字符AB,返回一种添加最少字符,使其变成回文的一种方案,返回ABA或者BAB均可 解题思路 准备一个res数组,长度是字符s本身的长度+dp[0][n-1] 运用双指针的想法,给res填充字符 当str[i]==str[j]时,res直接追加即可 当str[i]!=str[j]时,需要找到一个最少添加字符的方案 当dp[i][j - 1] < dp[i + 1][j]时,即最终在生成字符的时候,在左侧填充一个str[j] 当dp[i][j - 1] >= dp[i + 1][j]时,即最终在生成字符的时候,在右侧填充一个str[i] 复杂度分析 时间复杂度:O(N^2),N是字符的长度,求res的过程是O(N),总体是O(N^2) 空间复杂度:O(N^2),dp表的空间 3.Step3 给定一个字符串str,再给定str的最长回文子序列字符串strlps,请返回在添加字符最少的情况下,让str整体都是回文字符串的一种结果。进阶问题比原问题多了一个参数,请做到时间复杂度比原问题的实现低 举例: str="A1B21C",strlps="121",返回"AC1B2B1CA"或者"CA1B2B1AC",总之,只要是添加的字符数最少,只返回其中一种结果即可 题目分析 这个问题是给出了一个seed字符,生成最少添加字符形成回文后的其中一种结果 解题思路 运用剥洋葱的方式去解决 str的长度为nn,strlps的长度为m,形成回文res的结果是2*n-m ,因为m部分已经是回文了,不需要额外添加 以str=A1BC22DE1F,strlps=1221为例 洋葱的第0层是由strlps[0]与strlps[m-1],组成,即1...1,从str的最左侧开始找,一直找到strlps[0]=1的字符为止,记为leftPart,为A,接着从str的最右侧开始找,一直找到strlps[m-1]=1的字符为止,记为rightPart,为F, 将leftPart+rightPart的逆序leftPart+rightPart的逆序,复制到res的左侧 将rightPart+leftPart的逆序rightPart+leftPart的逆序,复制到res的右侧 结果变成了AF......FA 在加上strlps,变成了AF1......1FA 洋葱进入第1层,即2...2,接着第一层的位置找str,leftPart为BC,rightPart为DE, 将leftPart+rightPart的逆序leftPart+rightPart的逆序,复制到res的左侧 将rightPart+leftPart的逆序rightPart+leftPart的逆序,复制到res的右侧 结果变成了BCED......DECB 在加上strlpsstrlps,变成了BCED2......2DECB 加上上一层,变成了AF1BCED22DECB1FA 洋葱进入第n层。。。 代码部分注释掉了一部分,将resLeft = 0, resRight = 0;定义为全局变量,好理解些 整体实现逻辑是双指针,游走,不过左右指针有很多对,嗯,只能是,细节是魔鬼啊 方法1:Sort+Compare 先排序,题意要求连续序列,即可以比较nums[i]与 nums[i - 1],如果不相等,表示是递增的趋势,相等则反之,递增后需要判断是否连续,即相邻的元素差值是否为1 下面的代码处理边界case 如[-1,0],不会比较max与cur的值,需要在最后一道防线拦截一次 Math.max(max,cur); 时间复杂度:O(Nlog(N)),N是数组的长度,排序的复杂度 空间复杂度:O(1),常量级别的空间 方法2:Hash 准备一个set,首先将所有的num装进set for loop 数组,如果当前遍历到的元素num-1不在set中,说明这是一段新的可能出现的递增序列,变量curNum置为num,while循环判断curNum+1是否在set中,是则表示是连续的 记录max值 复杂度分析 时间复杂度:O(N),尽管在 for 循环中嵌套了一个 while循环,时间复杂度看起来像是二次方级别的。但其实它是线性的算法。因为只有当 curNum 遇到了一个序列的开始,$ while 循环才会被执行(也就是$ curNum-1 不在数组 nums 里), while循环在整个运行过程中只会被迭代N次。这意味着尽管看起来时间复杂度为 O(N^2), 实际这个嵌套循环只会运行 O(N+N)=O(N)次。所有的计算都是线性时间的,所以总的时间复杂度是O(N)的 空间复杂度:O(1),常量级别的空间 使用 O(1)的空间复杂度来处理,会比较麻烦,需要压缩空间,题意中的最长上升连续子序列,定义为从左到右和从右到左均可以 准备两个数组,大小都为2: start记录从左到右的连续递增子序列的最长长度 end记录从右到做的连续递增子序列的最长长度 对于0位置未添加空字符串 dp[i][j]表示的是s1[0...i-1]与s2[0...j-1]的最长公共子序列的长度,要求的是dp[m-1][n-1] 当s1[i]==s2[j],说明这两个字符是公共的字符,只要考察其子问题,dp[i-1][j-1]的长度即可,在此基础上+1, 当s1[i]!=s2[j],说明这两个字符不是公共的字符,只要考察其两个子问题,dp[i-1][j],dp[i][j-1],取max 动态转移方程: 对于0位置添加空字符串 dp[i][j]表示的是s1[0...i]与s2[0...j]的最长公共子序列的长度,要求的是dp[m][n] 注意s1|s2的位置是错位了一个,其长度达不到m|n 准备一个dp,n+1的长度,其实的0位置存放一个空字符串 准备几个变量: last:表示是当前dp[j](dp[i][j])左上角的数,相当于dp[i-1][j-1],初始化的时候为0 tmp:表示是当前dp[j](dp[i][j])正上方的数,相当于dp[i- 1][j] dp[j-1]:表示是当前dp[j](dp[i][j])左边的数,相当于dp[i][j-1] 每一轮结束后,last的值都向前滚动一个,变成正上方的数,也就是tmp 参考链接: https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-jie-zui-chang-zi-xu-lie-zi-chua-3/ https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/solution/dong-tai-gui-hua-jie-zui-chang-zi-xu-lie-zi-chua-4/ https://leetcode-cn.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/solution/dong-tai-gui-hua-jie-zui-chang-zi-xu-lie-zi-chuan-/ https://leetcode-cn.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/solution/dong-tai-gui-hua-jie-zui-chang-zi-xu-lie-zi-chuan-/ https://leetcode-cn.com/problems/longest-consecutive-sequence/solution/dong-tai-gui-hua-jie-zui-chang-zi-xu-lie-zi-chua-5/ https://leetcode-cn.com/problems/longest-common-subsequence/solution/a-fei-xue-suan-fa-zhi-by-a-fei-8/ public int lengthOfLISII(int[] nums) {
int n = nums.length;
int[] tails = new int[n];
int end = 0;
for (int i = 0; i < n; i++) {
int l = 0, r = end;
while (l < r) {
int m = (l + r) / 2;
if (tails[m] < nums[i]) l = m + 1;
else r = m;
}
tails[l] = nums[i];
if (end == r) end++;
}
return end;
}
动态规划解最长子序列子串等一类问题之最长连续递增序列[Reindeer]
public int findLengthOfLCIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);//至少可以与其自身形成递增序列
int max = 1;
for (int i = 1; i < n; i++) {
if (nums[i] > nums[i - 1]) {
dp[i] = dp[i - 1] + 1;
max = Math.max(max, dp[i]);
}
}
return max;
}
public int findLengthOfLCIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int max = 1;
int[] dp = new int[2];
dp[0] = 1;
for (int i = 1; i < n; i++) {
dp[i % 2] = 1;//前一个状态值都会被覆盖,需要重新初始化
if (nums[i] > nums[i - 1]) {
dp[i % 2] += dp[(i - 1) % 2];//当前状态依赖前一状态,需要再前一状态上累加
}
max = Math.max(max, dp[i % 2]);
}
return max;
}
public int findNumberOfLIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] dp = new int[n];
int[] counter = new int[n];
Arrays.fill(dp, 1);
Arrays.fill(counter, 1);
int max = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
counter[i] = counter[j];
} else if (dp[j] + 1 == dp[i]) {
counter[i] += counter[j];
}
}
}
max = Math.max(max, dp[i]);
}
int res = 0;
for (int i = 0; i < n; i++) {
if (dp[i] == max) res += counter[i];
}
return res;
}
动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]
1.Step1
public int minInsertions(String s) {
int[][] dp = buildDP(s.toCharArray());
return dp[0][s.length() - 1];
}
public int[][] buildDP(char[] chas) {
// $dp[i][j]$表示子串$str[i...j]$范围内的最少添加多少个字符后,可以形成回文子串
int n = chas.length;
int[][] dp = new int[n][n];
for (int j = 1; j < n; j++) {
dp[j - 1][j] = (chas[j - 1] == chas[j]) ? 0 : 1;
for (int i = j - 2; i >= 0; i--) {
if (chas[i] == chas[j]) dp[i][j] = dp[i + 1][j - 1];
else dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
}
}
return dp;
}
复杂度分析
2.Step2
public String getPalindromeI(String s) {
char[] chas = s.toCharArray();
int n = chas.length;
int[][] dp = buildDP(chas);
int appendLen = dp[0][n - 1];
char[] res = new char[n + appendLen];
int i = 0, j = n - 1;
int left = 0, right = res.length - 1;
while (i <= j) {
if (chas[i] == chas[j]) {
res[left++] = chas[i++];
res[right--] = chas[j--];
} else {
if (dp[i][j - 1] < dp[i + 1][j]) {
res[left++] = chas[j];
res[right--] = chas[j--];
} else if (dp[i][j - 1] >= dp[i + 1][j]) {
res[left++] = chas[i];
res[right--] = chas[i++];
}
}
}
return String.valueOf(res);
}
//res array 的左右指针,定义为全局变量
int resLeft = 0, resRight = 0;
public String getPalindromeII(String str, String strlps) {
char[] chas = str.toCharArray();//str 目标字符串
char[] lps = strlps.toCharArray();//base 字符串
int n = chas.length, m = lps.length;//len
char[] res = new char[2 * n - m];//res字符串 arr
int chasLeft = 0, chasRight = n - 1;//str 的左右指针
int lpsLeft = 0, lpsRight = m - 1;//base 的左右指针
resRight = res.length - 1;
int tmpLeft = 0, tmpRight = 0;//tmp 左右指针
while (lpsLeft <= lpsRight) {//循环遍历lps字符
tmpLeft = chasLeft;
tmpRight = chasRight;
while (chas[chasLeft] != lps[lpsLeft]) chasLeft++;//如果lpsLeft在chas未出现,一直chasLeft++
while (chas[chasRight] != lps[lpsRight]) chasRight--;//如果lpsRight在chas未出现,一直chasRight--
build(res, chas, chasLeft, chasRight, tmpLeft, tmpRight);//组装
// int len = chasLeft - tmpLeft + tmpRight - chasRight;
// resLeft += len;
// resRight -= len;
res[resLeft++] = chas[chasLeft++];//添加当前的lpsLeft ==lpsRight对于的字符串
res[resRight--] = chas[chasRight--];//添加当前的lpsLeft ==lpsRight对于的字符串
lpsLeft++;//进入下一轮
lpsRight--;
}
return String.valueOf(res);
}
/**
* @param res 结果arr
* // * @param resLeft
* // * @param resRight
* @param chas str arr
* @param chasLeft str arr left pointer
* @param chasRight str arr right pointer
* @param tmpLeft tmp left pointer
* @param tmpRight tmp right pointer
*/
private void build(char[] res,
char[] chas, int chasLeft, int chasRight,
int tmpLeft, int tmpRight) {
for (int i = tmpLeft; i < chasLeft; i++) {
res[resLeft++] = chas[i];
res[resRight--] = chas[i];
}
for (int j = tmpRight; j > chasRight; j--) {
res[resLeft++] = chas[j];
res[resRight--] = chas[j];
}
}
动态规划解最长子序列子串等一类问题之最长连续子序列[White-lipped Deer]
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) return 0;
Arrays.sort(nums);
int n = nums.length;
int max = 1, cur = 1;
for (int i = 1; i < n; i++) {
if (nums[i] != nums[i - 1]) {
if (nums[i - 1] + 1 == nums[i]) cur++;
else {
max = Math.max(max, cur);
cur = 1;
}
}
}
return Math.max(max, cur);
}
复杂度分析
public int longestConsecutive(int[] nums) {
Set
最长上升连续子序列
O(n)
时间和 O(1)
额外空间来解决public int longestIncreasingContinuousSubsequence(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
//从左到右,start
int[] start = new int[2];
Arrays.fill(start, 1);
int maxStart = 1;
for (int i = 1; i < n; i++) {
start[i % 2] = 1;
if (nums[i] > nums[i - 1]) {
start[i % 2] += start[(i - 1) % 2];
}
maxStart = Math.max(maxStart, start[i % 2]);
}
//从右到做,end
int[] end = new int[2];
Arrays.fill(end, 1);
int maxEnd = 1;
for (int i = n - 2; i >= 0; i--) {
end[i % 2] = 1;
if (nums[i] > nums[i + 1]) {
end[i % 2] += end[(i + 1) % 2];
}
maxEnd = Math.max(maxEnd, end[i % 2]);
}
return Math.max(maxStart, maxEnd);
}
动态规划解最长子序列子串等一类问题之最长公共子序列[Hog Deer]
方法1:
DP
基础版 public int longestCommonSubsequence(String text1, String text2) {
if (text1 == null || text2 == null || text1.length() == 0 || text2.length() == 0) return 0;
char[] chas1 = text1.toCharArray();
char[] chas2 = text2.toCharArray();
int m = chas1.length, n = chas2.length;
int[][] dp = new int[m][n];
dp[0][0] = chas1[0] == chas2[0] ? 1 : 0;
for (int i = 1; i < m; i++) dp[i][0] = chas1[i] == chas2[0] ? 1 : dp[i - 1][0];
for (int j = 1; j < n; j++) dp[0][j] = chas1[0] == chas2[j] ? 1 : dp[0][j - 1];
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
if (chas1[i] == chas2[j]) dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
return dp[m - 1][n - 1];
}
方法2:
DP
优化版public int longestCommonSubsequence2nd(String text1, String text2) {
if (text1 == null || text2 == null || text1.length() == 0 || text2.length() == 0) return 0;
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
return dp[m][n];
}
方法3:
DP
压缩空间版O(1)
public int longestCommonSubsequence3rd(String text1, String text2) {
if (text1 == null || text2 == null ||
text1.length() == 0 || text2.length() == 0) return 0;
int m = text1.length(), n = text2.length();
int[] dp = new int[n + 1];
int tmp = 0;
for (int i = 1; i <= m; i++) {
int last = 0;
for (int j = 1; j <= n; j++) {
tmp = dp[j];
if (text1.charAt(i - 1) == text2.charAt(j - 1)) dp[j] = last + 1;
else dp[j] = Math.max(tmp, dp[j - 1]);
last = tmp;
}
// System.out.println(JSON.toJSONString(dp));
}
return dp[n];
}