题目描述:
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例: 输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 说明: 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。 你算法的时间复杂度应该为 O(n2) 。 进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
思路分析:(题解来自:https://leetcode-cn.com/u/liweiwei1419/)
动态规划,时间复杂度为 O(N^2);
“动态规划”的两个步骤是思考“状态”以及“状态转移方程”。
有的资料又将“动态规划”分为 3 步:
base case:思考问题规模最小的时候,是什么情况;
update function:自下而上思考这个问题,即上面的“状态转移方程”;
gola:重点强调了输出是什么,很多时候输出并不一定是最后一个状态。
我觉得这种分法更细致一点,“状态”以及“状态转移方程”也没有问题,但是我觉得还要加上一个,思考一下“输出”是什么,即将第 2 种的第 3 步加上去,在下面的分析中,我还会强调这一点。
1、定义状态
首先我们考虑能否将题目的问法定义成状态,即 dp[i] 表示长度为 i 的最长上升子序列的长度,但仔细思考之后,我们发现:由于“子序列”不要求连续,长度为 i - 1 的最长上升子序列,与长度为 i 的“最长上升子序列之间的递推关系并不那么容易得到。
但我们由「力扣」第 3 题:“无重复字符的最长子串”以及「力扣」第 53 题:“最大子序和”这两个问题的经验,再结合题意,可以知道,“上升”的递推关系是:看子序列最后一个数,如果一个新数,比子序列最后一个数还大,那么就可以放在这个子序列的最后,形成一个更长的子序列。反正一个子序列一定会以一个数字结尾,那我就将状态成以 nums[i] 结尾的“最长上升子序列”的长度,这一点是常见的。
dp[i]:表示以第 i 个数字为结尾的“最长上升子序列”的长度。即在 [0, ..., i] 的范围内,选择 以数字 nums[i] 结尾 可以获得的最长上升子序列的长度。注意:以第 i 个数字为结尾,即 要求 nums[i] 必须被选取。
初始化的时候,因为每个元素自己可以认为是一个长度为 1 的子序列,所以可以将 dp 数组的值全部设置为 1。
定义输出:下面要考虑一下输出,由于状态不是题目中的问法,因此不能将最后一个状态作为输出,这里输出是把 dp[0]、dp[1]、……、dp[n - 1] 全部看一遍,取最大值。
2、推导“状态转移方程”
遍历到索引是 i 的数的时候,根据上面“状态”的定义,考虑把 i 之前的所有的数都看一遍,只要当前的数 nums[i] 严格大于之前的某个数,那么 nums[i] 就可以接在这个数后面形成一个更长的上升子序列。因此,dp[i] 就是之前严格小于 nums[i] 的“状态”最大值加 1。
因此,状态转移方程是:
dp[i] = max{1 + dp[j] for j < i if nums[j] < nums[i]}
代码实现:
class Solution { public static int lengthOfLIS(int[] nums) { int len = nums.length; int[] dp = new int[len]; Arrays.fill(dp, 1); //初始化dp数组,每个元素至少都是以它自身为结尾,长度为1的自序列 int maxLen = 0; //从第二个元素开始 for (int i = 1; i < len; i++) { //以当前元素为结尾 int curVal = nums[i]; for (int j = 0; j < i; j++) { //当前元素严格大于之前的任何一个片段,则当前元素都可以加在这个区间后面,形成+1长度的自序列 if (curVal > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } } for (int element : dp) { maxLen = Math.max(maxLen, element); } return maxLen; } }
时间复杂度:O(n^2)
空间复杂度:O(n)