子序列 是值 一个字串 中 非连续 的 字串 ;字串: 123456789 子序列a: 13579(非连续)
字串b : 12345(连续)
** 子串 问题
** 1.求最大子串 **
class Solution {
public int lengthOfLIS(int[] nums) {
int get=vector(nums,0,nums.length-1);
return get;
}
//就最大子串
public int vector(int[] nums,int begin,int end){
int max=0;//输出最大子串的和
int curr=0;
int newBegin=0;
for(int i=0; i <nums.length ; i++){
curr += nums[i];
if(curr > max)
{
max=curr;
begin =newBegin;
end=i;
}
if(curr < 0)
{
curr=0;
newBegin=i+1;
}
}
return max;
}
}
// 找 最大子序列的 方法很简单 ,只要前 i 项 的和 还没有 小于 0 那么 子序列 就 一直向后扩展
// 小于 0 就 丢弃之前的 子序列 并且开始 新的 子序列
** 2.最长公共子串 **
**
** 子序列问题 **
首先让们想一下,有一个字串A:1 2 3 4 5 6 7 8 9,我们将它放在一个数组B里面,从B[0]开始放,
有一个数组f[9]用来记录字串从1到9的每个数的递增数;比如说:f[4]里面放的就是到5的最长递增数,f[4]的值就是5;现在我们想想这个数是怎么得到的,是不是依次将f[0]到f[3]的大小比较一遍,选出其中一个最大的数+1赋值给f[4];同时别忘了要满足B[4]>B[3]。现在让我们来实现我所说的这段话的代码。
1.递推法 解决 最长递增子序列问题
(不推荐)
int list(float[] B)
{
int n=B.length;
int[] f=new int[n];
f[0]=1;
for(int i=1; i< n; i++)
{
f[i] =1;
for(int j=0 ; j <i ; j++){
if(B[j] < B[i] && f[j] > f[i]-1 )
//作用是 比大小然后满足递增
f[i]=f[j]+1;
}
}
return f[n-1];
}
一, 最长递增子序列问题的描述
dp[i]:[0, i] 的最长上升子序列的长度。
dp[i] : 以 nums【i】 结尾 的 子序列 ,能达到 的 最大长度。
对于 dp【i】 来说,只要 找到 前面 比 nums【i】 小 的 nums【j】 中 最大 的 dp【j】即可。
动态转移方程
dp[i] = max(dp[i], dp[j] + 1); nums[i] > nums[j]
为啥你能得出这个方程?
首先要知道,如果 nums[i] 严格大于在它之前的某个数,那么 nums[i] 就可以接在这个数后面,形成一个更长的上升子序列。
所以我们每次都要在 [0, i) 这个区间内去找。那么定义 j,然后在 [0,i) 区间去找比 num[i] 小的数。区分为两种情况:
当 nums[i] > nums[j] :上升趋势,符合题意,dp[j] + 1;
当 nums[i] <= nums[j] :非上升趋势,不符合题意,跳过。
上述所有 1. 情况 下计算出的 dp[j] + 1 的最大值,为直到 i 的最长上升子序列长度(dp[i])。实现方式为遍历 j 时,每轮执行 dp[i] = max(dp[i], dp[j] + 1)。
初始化:
数组中每个元素都至少可以单独成为子序列,长度最少都为 1,所以初始化全为 1。
class Solution {
public int lengthOfLIS(int[] nums) {
int len =nums.length;
int ans=1;
//dp[i] : 以 nums【i】 结尾 的 子序列 ,能达到 的 最大长度。
int dp[] = new int[len];
//边界
dp[0] =1;
//复杂度(n的平方)
for(int i=1 ;i < len ; i++ )
{
dp[i]=1;
//找到前面结点里,比 num是【i】 小 的中,dp[] 最长的值
int maxPre=0;
for(int j= i-1 ;j>=0 ; j--)
{
if ( num[i] > nums[j] {
//dp【j】最优子结构 例如 : 数组 1 2 3 4 i为3 j为 0 1 2 求出 j中的最大值
maxPre=Math.max( dp[j], maxPre);
}
}
}
//状态转移方程
dp[i] = maxPre +1;
ans= Math.max(ans,dp[i]);
}
return ans;
}
复杂度分析
时间复杂度:O(n^2)
),其中 nn 为数组 nums 的长度。动态规划的状态数为 n,计算状态 dp[i] 时,需要 O(n) 的时间遍历 dp[0 … i-1] 的所有状态,所以总时间复杂度为 O(n^2)。
空间复杂度:O(n),需要额外使用长度为 nn 的 dpdp 数组。
** 动态规划 + 二分法 + 贪心O(nlogn) **
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];
// 遍历第 1 个数,直接放在有序数组 tail 的开头
tail[0] = nums[0];
// end 表示有序数组 tail 的最后一个已经赋值元素的索引
int end = 0;
for (int i = 1; i < len; i++) {
// 【逻辑 1】比 tail 数组实际有效的末尾的那个元素还大
if (nums[i] > tail[end]) {
// 直接添加在那个元素的后面,所以 end 先加 1
end++;
tail[end] = nums[i];
} else {
// 使用二分查找法,在有序数组 tail 中
// 找到第 1 个大于等于 nums[i] 的元素,尝试让那个元素更小
int left = 0;
int right = end;
while (left < right) {
// 选左中位数不是偶然,而是有原因的,原因请见 LeetCode 第 35 题题解
// int mid = left + (right - left) / 2;
int mid = left + ((right - left) >>> 1);
if (tail[mid] < nums[i]) {
// 中位数肯定不是要找的数,把它写在分支的前面
left = mid + 1;
} else {
right = mid;
}
}
// 走到这里是因为 【逻辑 1】 的反面,因此一定能找到第 1 个大于等于 nums[i] 的元素
// 因此,无需再单独判断
tail[left] = nums[i];
}
// 调试方法
// printArray(nums[i], tail);
}
// 此时 end 是有序数组 tail 最后一个元素的索引
// 题目要求返回的是长度,因此 +1 后返回
end++;
return end;
}
// 调试方法,以观察是否运行正确
private void printArray(int num, int[] tail) {
System.out.print("当前数字:" + num);
System.out.print("\t当前 tail 数组:");
int len = tail.length;
for (int i = 0; i < len; i++) {
if (tail[i] == 0) {
break;
}
System.out.print(tail[i] + ", ");
}
System.out.println();
}
public static void main(String[] args) {
int[] nums = new int[]{3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12};
Solution solution = new Solution8();
int lengthOfLIS = solution8.lengthOfLIS(nums);
System.out.println("最长上升子序列的长度:" + lengthOfLIS);
}
}
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-er-fen-cha-zhao-tan-xin-suan-fa-p/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 确定dp数组(dp table)以及下标的含义
这道题目我们要一起维护两个数组。
dp[i]:i之前(包括i)最长递增子序列的长度为dp[i]
count[i]:以nums[i]为结尾的字符串,最长递增子序列的个数为count[i]
- 确定递推公式
在300.最长上升子序列 中,我们给出的状态转移是:
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
即:位置i的最长递增子序列长度 等于j从0到i-1各个位置的最长升序子序列 + 1的最大值。
本题就没那么简单了,我们要考虑两个维度,一个是dp[i]的更新,一个是count[i]的更新。
那么如何更新count[i]呢?
以nums[i]为结尾的字符串,最长递增子序列的个数为count[i]。
那么在nums[i] > nums[j]前提下,如果在[0, i-1]的范围内,找到了j,使得dp[j] + 1 > dp[i],说明找到了一个更长的递增子序列。
那么以j为结尾的子串的最长递增子序列的个数,就是最新的以i为结尾的子串的最长递增子序列的个数,即:count[i] = count[j]。
在nums[i] > nums[j]前提下,如果在[0, i-1]的范围内,找到了j,使得dp[j] + 1 == dp[i],说明找到了两个相同长度的递增子序列。
那么以i为结尾的子串的最长递增子序列的个数 就应该加上以j为结尾的子串的最长递增子序列的个数,即:count[i] += count[j];
代码如下:
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
dp[i] = max(dp[i], dp[j] + 1);
}
当然也可以这么写:
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1; // 更新dp[i]放在这里,就不用max了
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
}
这里count[i]记录了以nums[i]为结尾的字符串,最长递增子序列的个数。dp[i]记录了i之前(包括i)最长递增序列的长度。
题目要求最长递增序列的长度的个数,我们应该把最长长度记录下来。
代码如下:
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > maxCount) maxCount = dp[i]; // 记录最长长度
}
}
3. dp数组如何初始化
再回顾一下dp[i]和count[i]的定义
count[i]记录了以nums[i]为结尾的字符串,最长递增子序列的个数。
那么最少也就是1个,所以count[i]初始为1。
dp[i]记录了i之前(包括i)最长递增序列的长度。
最小的长度也是1,所以dp[i]初始为1。
代码如下:
vector dp(nums.size(), 1);
vector count(nums.size(), 1);
其实动规的题目中,初始化很有讲究,也很考察对dp数组定义的理解。
- 确定遍历顺序
dp[i] 是由0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是0到i-1,遍历i的循环里外层,遍历j则在内层,代码如下:
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > maxCount) maxCount = dp[i];
}
}
最后还有再遍历一遍dp[i],把最长递增序列长度对应的count[i]累计下来就是结果了。
代码如下:
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > maxCount) maxCount = dp[i];
}
}
int result = 0; // 统计结果
for (int i = 0; i < nums.size(); i++) {
if (maxCount == dp[i]) result += count[i];
}
统计结果,可能有的同学又有点看懵了,那么就再回顾一下dp[i]和count[i]的定义。
- 举例推导dp数组
输入:[1,3,5,4,7]
如果代码写出来了,怎么改都通过不了,那么把dp和count打印出来看看对不对!
时间复杂度O(n^2)
空间复杂度O(n)
还有O(nlogn)的解法,使用树状数组,今天有点忙就先不写了,感兴趣的同学可以自行学习一下,这里有我之前写的树状数组系列博客 (十年前的陈年老文了)
class Solution {
public int findNumberOfLIS(int[] nums) {
if (nums.length <= 1) return nums.length;
int[] dp = new int[nums.length];
for(int i = 0; i < dp.length; i++) dp[i] = 1;
int[] count = new int[nums.length];
for(int i = 0; i < count.length; i++) count[i] = 1;
int maxCount = 0;
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
}
if (dp[i] > maxCount) maxCount = dp[i];
}
}
int result = 0;
for (int i = 0; i < nums.length; i++) {
if (maxCount == dp[i]) result += count[i];
}
return result;
}
}
作者:carlsun-2
链接:https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-dp673-9txt/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。