-
LeetCode链接:300. 最长上升子序列
-
算法策略 : 动态规划(Dynamic Programming)
给定一个无序的整数序列,求出它最长上升子序列的长度(要求严格上升)
比如 [10,2,2,5,1,7,101,18] 的最长上升子序列是 [2,5,7,101]、[2,5,7,18],长度是4
- 最长上升子序列(最长递增子序列,Longest Increasing Subsequence,LIS)
解法1 - 动态规划
状态定义
- 假设数组是nums,[10,2,2,5,1,7,101,18]
dp(i)是以nums[i]结尾的最长上升子序列的长度,i ∈ [0,nums.length)
- 以nums[0] 10结尾的最长上升子序列是10,所以dp(0) = 1
- 以nums[1] 2结尾的最长上升子序列是2,所以dp(1) = 1
- 以nums[2] 2结尾的最长上升子序列是2,所以dp(2) = 1
- 以nums[3] 5结尾的最长上升子序列是2、5,所以dp(3) = dp(1) + 1 = dp(2) + 1 = 2
- 以nums[4] 1结尾的最长上升子序列是1,所以dp(4) = 1
- 以nums[5] 7结尾的最长上升子序列是2、5、7,所以dp(5) = dp(3) + 1 = 3
- 以nums[6] 101结尾的最长上升子序列是2、5、7、101,所以dp(6) = dp(5) + 1 = 4
- 以nums[7] 18结尾的最长上升子序列是2、5、7、18,所以dp(7) = dp(5) + 1 = 4
- 最长上升子序列的长度是所有dp(i)中最大值max { dp(i) }, i ∈ [0,nums.length)
状态转义方程
- 遍历 j ∈ [0,i)
- 当nums[i] > nums[j]
nums[i]可以接在nums[j]后面,行程一个比dp(j)更长的上升子序列,长度为dp(j) + 1
dp(i) = max { dp(i), dp(j) + 1 } - 当nums[i] <= nums[j]
nums[i]不能接在nums[j]后面,跳过此次遍历(continue) - 状态的初始值
dp(0) = 1
所有的 dp(i) 默认都初始化为1
实现
int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
int max = dp[0] = 1;
for (int i = 1; i < dp.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++ ) {
if (nums[i] <= nums[j]) continue;
dp[i] = Math.max(dp[i], dp[j] + 1);
}
max = Math.max(dp[i], max);
}
return max;
}
- 时间复杂度:O(n),时间复杂度:O(n^2)
解法2 - 二分搜索
- 把每个数字看作是一张扑克牌,从左到右按顺序处理每一张扑克牌
将它压在(从左边数过来)第一个牌顶>=它的排队上面
如果找不到牌顶>=它的牌堆,就在左右边新建一个牌堆,将它放入这个新牌堆中
- 当处理完所有牌,最终牌堆的数量就是最长上升子序列的长度
- 思路(假设数组是nums,也就是最初的牌数组)
top[i] 是第i个牌堆的牌顶,len是牌堆的数量,初始值为0
遍历每一张牌num
- 利用二分搜索找出num最终要放入的牌堆位置index
- num作为第index个牌堆的牌顶,top[index] = num
- 如果index等于len,相当于新建一个牌堆,牌堆数量 +1,也就是len++
实现
int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] top = new int[nums.length];
int len = 0;
for (int num : nums) {
int begin = 0, end = 0;
while (begin < end) {
int mid = (begin + end) >> 1;
if (num <= top[mid]) {
end = mid;
} else {
begin = mid + 1;
}
}
top[begin] = num;
if (begin == len) len++;
}
return len;
}
- 空间复杂度:O(n),时间复杂度O(nlogn)