最长上升子序列(LIS)问题
300. 最长递增子序列
题目要求:给定整数数组nums
,找到其中最长严格递增子序列的长度,子序列代表原序列中删除(不删除)数组中的元素而不改变其他元素的顺序。
解题思路
解法一:DFS+一维记忆化
- 从每个元素往下搜索满足的最长递增子序列长度
- 加一个缓存,记录已搜索过的起点,例如
2->3->5
,搜索起点为2的时候,起点为3的最长递增子序列也搜索了
时间复杂度:O(n2) ,每个元素进行遍历后面的元素
空间复杂度:O(n),即memo数组的占用空间,递归占用不会超过O(n)
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n <= 1) {
return n;
}
int[] memo = new int[n];
int ans = 1;
for (int i = 0; i < n; i++) { // 每个起点开始寻找
// 已经搜过的起点跳过,因为最初标记的起点肯定最长
if (memo[i] == 0) {
ans = Math.max(ans, dfs(i, nums, memo));
}
}
return ans;
}
public int dfs(int index, int[] nums, int[] memo) {
if (memo[index] != 0) {
return memo[index];
}
int res = 1;
for (int i = index + 1; i < nums.length; i++) { // 继续寻找比当前更大的元素
if (nums[i] > nums[index]) {
res = Math.max(res, 1 + dfs(i, nums, memo));
}
}
memo[index] = res;
return res;
}
}
解法二:动态规划
-
状态定义:
dp[i]
表示以nums[i]
结尾的最长递增子序列长度,这样在计算后面值的时候可以利用前面计算好的值 -
状态转移讨论:设 j∈[0,i) ,每轮计算新 dp[i] 时,遍历 [0,i) ,做一下判断:
-
当 nums[i]>nums[j] 时: nums[i] 可以接到 nums[j] 的后面,满足严格递增,此时最长递增子序列长度为 dp[j]+1
-
当 nums[i] <= nums[j] 时: nums[i] 无法接到 nums[j] 的后面,不满足最长递增子序列,跳过
-
对于所有
1
情况计算出的 dp[j]+1 最大值,为直到 i 的最长递增子序列长度 -
转移方程: dp[i] = max(dp[i], dp[j] + 1) ,其中 j∈[0,i) , nums[i]>nums[j]
- 初始状态: dp[i] 的所有元素为1,因为每个元素都可以称为子序列
- 返回值:返回dp列表的最大值,即全局最长递增子序列长度
时间复杂度:O(n2),每个元素都要遍历之前的元素
空间复杂度:O(n),即dp数组占用空间
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n <= 1) {
return n;
}
int[] dp = new int[n];
Arrays.fill(dp, 1);
int res = 0;
for (int i = 1; i < n; 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;
}
}
解法三:贪心解
贪心思想:可以使用数组来记录之前出现最长的递增子序列,一旦出现比之前元素还小的元素时,把比之前大的元素给替换掉,以便容纳更多小些的元素,使得子序列递增的更缓慢一些。举个栗子,对于数组[0,1,0,3,2,3]
, 依次遍历,对应的记录数组变化
- i=0, g=[0],记录数组为空,直接加进来
- i=1, g=[0,1],比记录数组末尾的数大,接在后面
- i=2, g=[0,0],比记录数组中的数小,遍历找出对应位置替换
- i=3, g=[0,0,3],比记录数组末尾的数大,接在后面
- i=4, g=[0,0,2],比记录数组中的数小,遍历找出对应位置替换
- i=5,g=[0,0,2,3],比记录数组末尾的数大,接在后面
- 因此结果为记录数组的最大长度
时间复杂度:O(n2)
空间复杂度:O(n)
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n <= 1) {
return n;
}
int[] g = new int[n];
g[0] = nums[0];
int size = 1;
for (int i = 1; i < n; i++) {
if (nums[i] > g[size - 1]) { // 接到上一个最长递增子序列的后面
g[size++] = nums[i];
} else {
int r = size - 1;
while (r >= 0 && g[r] >= nums[i]) { // 遍历找到比当前值小的索引
r--;
}
g[r + 1] = nums[i]; // 当前值小的数后面接上
}
}
return size;
}
}
解法四:贪心解+二分
基于解法四进行优化,因为记录数组时递增的,因此适合在查找合适位置时使用二分查找
时间复杂度:O(nlogn)
空间复杂度:O(n)
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n <= 1) {
return n;
}
int[] g = new int[n];
g[0] = nums[0];
int size = 1;
for (int i = 1; i < n; i++) {
if (nums[i] > g[size - 1]) {
g[size++] = nums[i];
} else {
int l = 0, r = size - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (g[mid] >= nums[i]) { // 目标为左边界(查询大于等于nums[i]的值进行替换)
r = mid - 1;
} else {
l = mid + 1;
}
}
g[l] = nums[i]; // 左边界索引赋值
}
}
return size;
}
}
本文来自博客园,作者:LogBiao,转载请注明原文链接:https://www.cnblogs.com/logbiao/p/16476098.html