地址:https://leetcode-cn.com/problems/longest-increasing-subsequence/
我写的题解地址:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-er-fen-cha-zhao-tan-xin-suan-fa-p/
首先,需要对子序列和子串进行区分;
[4, 6, 5]
是 [1, 2, 4, 3, 7, 6, 5]
的一个子序列。其次,题目中的“上升”要求严格“上升”,即不能有重复元素;
第三,子序列中元素的相对顺序很重要,它们必须保持在原始数组中的相对顺序不变。否则这道题去重以后,元素的个数即为所求。
首先思考暴力解法,使用“回溯搜索算法”或者“位运算”的技巧,得到原始数组的所有可能的子序列(时间复杂度 O ( 2 N ) O(2^N) O(2N)),对这些子串再依次判定是否为“严格”上升,(时间复杂度 O ( N ) O(N) O(N)),总的时间复杂度为: O ( N 2 N ) O(N2^N) O(N2N)。
这道题是经典的使用动态规划解决的问题,在这里我尝试解释使用动态规划解决这个问题的思路。
从一个较短的上升子序列得到一个较长的上升子序列,我们主要关心这个较短的上升子序列的结尾元素。
为了保证子序列的相对顺序性,在程序读到一个新的数的时候,如果比已经得到的子序列的最后一个数还大,那么就可以放在这个子序列的最后,形成一个更长的子序列。
一个子序列一定会以一个数结尾,于是将状态定义成:dp[i]
表示以 nums[i]
结尾的“最长上升子序列”的长度,注意这个定义中 nums[i]
必须被选取,且必须被放在最后一个元素。
遍历到 nums[i]
时,考虑把索引 i
之前的所有的数都看一遍,只要当前的数 nums[i]
严格大于之前的某个数,那么 nums[i]
就可以接在这个数后面形成一个更长的上升子序列。因此,dp[i]
就等于索引 i
之前严格小于 nums[i]
的状态最大者 + 1 +1 +1。
语言描述:在索引 i
之前严格小于 nums[i]
的所有状态中的最大者 + 1 + 1 +1。
符号描述:
d p [ i ] = max 0 ≤ j < i , n u m s [ j ] < n u m s [ i ] d p [ j ] + 1 dp[i] = \max_{0 \le j < i, nums[j] < nums[i]} {dp[j] + 1} dp[i]=0≤j<i,nums[j]<nums[i]maxdp[j]+1
dp[0] = 1
,1 个字符当然也是长度为 1 的上升子序列;dp[i]
中的最大值(dp[i]
考虑了所有以 nums[i]
结尾的上升子序列);max 1 ≤ i ≤ N d p [ i ] \max_{1 \le i \le N} dp[i] 1≤i≤Nmaxdp[i]
Java 代码:
import java.util.Arrays;
public class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
if (len < 2) {
return len;
}
int[] dp = new int[len];
Arrays.fill(dp, 1);
for (int i = 1; i < len; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int res = 0;
for (int i = 0; i < len; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
}
复杂度分析:
for
循环,每个 for
循环的时间复杂度都是线性的。这个动态规划的方法在计算一个新的状态的时候,需要考虑到之前所有小于 nums[i]
的那些位置的状态。事实上,这个算法还有改进的空间。
依然是着眼于一个上升子序列的结尾元素。思路是这样的:
如果已经得到的上升子序列的结尾的数越小,遍历的时候后面接上一个数,就会有更大的可能性构成一个更长的上升子序列。
说明:
1、在最开始,我们强调了子序列的定义,必须保持子序列中的元素在原始数组中的相对顺序。因此,通过从左向右遍历得到一个上升子序列,这个方法是合理的;
2、既然结尾越小越好,可以如下定义状态。为了与之前的状态区分,这里将状态数组命名为 tail
。
tail[i]
表示长度为 i + 1
的所有最长上升子序列的结尾的最小值。
说明: 1、状态定义其实也描述了状态转移方程;
2、以题目中的示例为例 [10, 9, 2, 5, 3, 7, 101, 18]
中,容易发现长度为 2
的所有上升子序列中结尾最小的是 [2, 3]
,因此 tail[1] = 3
。
从直觉上看,数组 tail
也是一个严格上升数组。
下面证明:即对于任意的索引 i
< j
,都有 tail[i] < tail[j]
。
使用反证法:
假设对于任意的索引 i
< j
,存在某个 tail[i] >= tail[j]
。
对于此处的 tail[i]
而言,对应一个上升子序列 [ a 0 , a 1 , . . . , a i ] [a_0, a_1, ..., a_i] [a0,a1,...,ai],依据定义 t a i l [ i ] = a i tail[i] = a_i tail[i]=ai;
对于此处的 tail[j]
而言,对应一个上升子序列 [ b 0 , b 1 , . . . , b i , . . . , b j ] [b_0, b_1, ..., b_i, ... , b_j] [b0,b1,...,bi,...,bj],依据定义 t a i l [ j ] = b j tail[j] = b_j tail[j]=bj;
由于 tail[i] >= tail[j]
,等价于 a i ≥ b j a_i \ge b_j ai≥bj,而在上升子序列 [ b 0 , b 1 , . . . , b i , . . . , b j ] [b_0, b_1, ..., b_i, ... , b_j] [b0,b1,...,bi,...,bj] 中, b i b_i bi 严格小于 b j b_j bj,故有 a i ≥ b j > b i a_i \ge b_j > b_i ai≥bj>bi,则上升子序列 [ b 0 , b 1 , . . . , b i ] [b_0, b_1, ..., b_i] [b0,b1,...,bi] 是一个长度也为 i + 1
但是结尾更小的数组,与 a i a_i ai 的最小性矛盾。因此原命题成立。
因此,我们只需要维护有序数组 tail
,它的长度就是最长上升子序列的长度。下面说明如何在遍历中维护有序数组 tail
的定义:
遍历到新的数 nums[i]
时,首先看 nums[i]
是否严格大于有序数组 tail
的末尾元素。
如果 tail[len - 1] < nums[i]
,则 tail[len] = nums[i]
,得到一个更长的上升子序列;
如果数组 tail
中有元素等于 nums[i]
什么都不操作,新遍历到的数不会使得已有的子序列的长度更长;
剩下的情况是,有序数组 tail
中一定有一个元素大于 nums[i]
,将第 1 个大于等于 nums[i]
位置的元素变小,这一步操作正是在维护数组 tail[i]
的定义。