最长上升子序列–LeetCode300
题目
给定一个无序的整数数组,找到其中最长上升子序列的长度。示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(nlog n) 吗?
方法一:动态规划
首先定义dp[i] 为考虑前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取 。从左往右计算dp数组中各个位置的值,状态转移方程为:
dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]
含义:
- 往dp[0…i−1] 中最长的上升子序列后面再加一个 nums[i]。
- 上面这一项的条件:由于 dp[j] 代表 nums[0…j] 中以 nums[j] 结尾的最长上升子序列,所以如果能从 dp[j]这个状态转移过来,那么 nums[i] 必然要大于 nums[j],才能将 nums[i] 放在 nums[j] 后面以形成更长的上升子序列。(例如:2,3,5,3,4,4这个位置上的最长子序列和5就没有关系了,因为4已经不可能接在5的后面构成最长上升子序列了)
故最终的结果就是dp数组中的最大值。
时间复杂度:O(n2),空间复杂度:O(n)
class Solution {
public int lengthOfLIS(int[] nums) {
// 边界条件
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];//dp数组用于保存每个位置上的最长上升子序列
dp[0] = 1;//首先第一个位置自身就是一个子序列,长度为1
// 结果至少是1,所以初始化为1
// maxans就是dp数组中的最大值
int maxans = 1;
// 第一层循环,对dp数组进行遍历,为了确定每个位置上的目标序列长度
for (int i = 1; i < dp.length; i++) {
int maxval = 0;
for (int j = 0; j < i; j++) {
// 下面的判断条件可以理解为:如果nums[j] >= nums[i]
// 那么nums[j]位置上的最长上升子序列和num[i]就没有关系了,
// 就不构成上升序列了
if (nums[j] < nums[i]) {
maxval = Math.max(maxval, dp[j]);
}
}
dp[i] = maxval+1;// 参考状态方程,上面if条件也是
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}
方法二:贪心+二分查找
首先需要明确方法二相对于方法一的动态规划,优化的地方在哪?
动态规划中,遍历计算 dp 数组需 O(N),计算每个 dp[k] 需 O(N)。
- 动态规划中,通过线性遍历来计算 dp 的复杂度无法降低;
- 每轮计算中,需要通过线性遍历 [0,k)区间元素来得到 dp[k] 。我们考虑:是否可以通过重新设计状态定义,使整个 dp 为一个排序列表;这样在计算每个 dp[k] 时,就可以通过二分法遍历 [0,k) 区间元素,将此部分复杂度由 O(N) 降至 O(logN)。
数组定义:tails[k] 的值代表 长度为 k+1 子序列 的尾部元素值。
转移方程 : 设 res为 tails 当前长度,代表直到当前的最长上升子序列长度。设 j∈[0,res),考虑每轮遍历 nums[k] 时,通过二分法遍历 [0,res) 列表区间,找出 nums[k] 的大小分界点,会出现两种情况:
- 区间中存在 tails[i]>nums[k] : 将第一个满足 tails[i]>nums[k] 执行 tails[i]=nums[k] ;因为更小的 nums[k] 后更可能接一个比它大的数字。
- 区间中不存在 tails[i]>nums[k] : 意味着 nums[k] 可以接在前面所有长度的子序列之后,因此肯定是接到最长的后面(长度为 res ),新子序列长度为 res+1。
举例说明:假设此时tail=[2,3,7],num[k]=5,那么[2,3,5]比[2,3,7]更有可能构成最长上升子序列,因为[2,3,5]上升得慢,所以把tail数组更新为[2,3,5];如果num[k]=8,8>7,那么只能把8加入tail数组中。对于查找的过程使用二分查找,例如tail=[2,3,7,10],num[k]=4,那么使用二分查找找到第一个大于4的位置将其更新为4,也就是把7更新为4。
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] tail = new int[nums.length];
int res = 0;//tail数组的当前长度
for (int num : nums) {
// 二分查找,寻找tail数组中元素第一个大于num的位置
int i = 0, j = res;
while (i < j) {
int mid = (i+j)>>1;
if (tail[mid] < num) {
i = mid+1;
}else {
j = mid;
}
}
tail[i] = num;// 注意这里为什么是i而不是j!从含义上去理解,二分查找找的是第一个大于num的位置
if (j == res) {
// 说明num比tail数组中所有元素都大,那么在tail数组长度加一
res++;
}
}
return res;
}
}