leetcode 300. 最长上升子序列
题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。 示例: 输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 说明: 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。 你算法的时间复杂度应该为 O(n2) 。 进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗? 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/longest-increasing-subsequence 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
代码
代码1. 动态规划
解题思路: 状态定义: dp[i] 代表 nums 前 i 个数字的最长子序列长度。 转移方程: 设 j∈[0,i),考虑每轮计算新 dp[i] 时,遍历 i 之前的 dp 列表区间(即 [0,i) 区间); 当 nums[i] > nums[j] 时: nums[i]可以接在 nums[j] 之后(此题要求严格递增),此情况下最长上升子序列长度为 dp[j] + 1 ; 当 nums[i] <= nums[j]时: nums[i]无法接在 nums[j]之后,此情况上升子序列不成立,跳过。 上述所有情况下计算出的 dp[j] + 1 的最大值,为直到 i 的最长上升子序列长度,实现方式为遍历 j 时每轮执行 dp[i] = max(dp[i], dp[j] + 1)。
初始状态:令 dp 列表所有值 =1,含义为每个数字单独组成序列时,长度都为 1 。
返回值:返回 dpdp 列表的最大值,即最终最长上升子序列。
复杂度分析: 时间复杂度 O(N^2): 遍历计算 dp 列表需 O(N) ,计算每个 dp[i] 需 O(N) 。 空间复杂度 O(N)O(N) : dpdp 列表占用线性大小额外空间。
粗暴总结:
遍历dp, 找 i 前面的所有小于 nums[i] 的数字的 max(dp)+1
代码
// Dynamic programming. public class Solution { public int lengthOfLIS(int[] nums) { int len = nums.length; if (len == 0) { return 0; } int[] dp = new int[len]; //dp[i] 代表 nums 前 i个数字的最长子序列长度。 dp[0] = 1; int max = 1; //保存最长上升序列长度 for (int i = 1; i < len; i++) { int maxj = 0; //保存 i 前面的所有小于 nums[i] 的数字中的最大dp[j]值; for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { maxj = Math.max(maxj, dp[j]); } } dp[i] = maxj + 1; max = Math.max(max, dp[i]); } return max; } }
精简版本
//Dynamic programming. class Solution { public int lengthOfLIS(int[] nums) { int len = nums.length; if (len == 0) { return 0; } int[] dp = new int[len]; int max = 0; Arrays.fill(dp, 1);//令 dp列表所有值 =1, 含义为每个数字单独组成序列时,长度都为 1 。 for(int i = 0; i < len; i++) { for(int j = 0; j < i; j++) { if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } max = Math.max(max, dp[i]); } return max; } }
代码2. 动态规划+二分查找
解题思路: 降低复杂度切入点: 遍历计算 dp 列表需 O(N) ,计算每个 dp[i] 需 O(N) 。 动态规划中,通过线性遍历来计算 dp 的复杂度无法降低; 每轮计算中,需要通过线性遍历 [0,i) 区间元素来得到 dp[i]。 改进思路: 设常量数字 N ,和随机数字 x ,我们可以容易推出:当 N 越小时,N<x 的几率越大。 重新设计状态定义,使整个 dp 为一个排序列表; 这样在计算每个 dp[i] 时,就可以通过二分法遍历 [0,i) 区间元素,将此部分复杂度由 O(N)降至 O(logN) 。 状态定义: dp[i] 的值代表 子序列的长度 为 i 时,此序列尾部元素的值。 转移方程:
遍历计算每个 dp[i],不断更新长度为 [1,i] 的子序列尾部元素值,始终保持每个尾部元素值最小 初始状态:
令 dp 列表所有值 =1 ,含义为每个数字单独组成序列时,长度都为 1 。 返回值:
返回 dp 列表的最大值,即最终最长上升子序列。
代码
//Dynamic programming + Dichotomy. class Solution { public int lengthOfLIS(int[] nums) { int[] dp = new int[nums.length];//dp[i] 的值代表 子序列的长度 为 i 时,此序列尾部元素的值。 int max = 0;// 最长子序列的长度 for(int num : nums) { int i = 0, j = max; while(i < j) { int m = (i + j) / 2; if(dp[m] < num) i = m + 1; else j = m; } dp[i] = num; //二分法找到大于num的第一个值覆盖 max = Math.max(max, i + 1);//更新最大长度 } return max; } }