300. 最长递增子序列
题目描述
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
进阶:
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
题解
动态规划
这题是动态规划中子序列问题的基本形式,老样子,首先确定:
dp
数组含义:dp[i]
表示原数组里以nums[i]
结尾的(前缀)子数组中,最长递增子序列的长度。- 状态转移方程:对于
0 <= j < i
,如果if (nums[i] > nums[j])
,说明当前的nums[i]
可以加入之前以nums[j]
结尾的递增子序列,得到新递增子序列长度即为dp[j] + 1
。在与原本以nums[i]
为结尾的子序列长度比较,取较大值即可:dp[i] = max(dp[i], dp[j] + 1)
。
最后,取 dp
中最大值即为所求。
该算法更详细的解析参见 代码随想录-300
代码(C++)
int lengthOfLIS(vector<int> &nums)
{
vector<int> dp(nums.size(), 1);
for (int i = 1; i < nums.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
}
}
return *max_element(dp.begin(), dp.end());
}
贪心算法 + 二分查找
上面的动态规划解法时间复杂度为 O ( n 2 ) O(n^2) O(n2) ,如果要满足题目进阶要求的 O ( n l o g n ) O(n log{n}) O(nlogn) ,则可以采用贪心算法结合二分查找解决。
根据题目,我们或产生一种 “感性” 的认识:要让递增子序列尽可能长,它增长的幅度应该尽可能 “缓” ,即每次增加到该子序列 末尾 的数字 虽然要大于前面所有的数(保证递增),但也要尽可能小 ——例如,假设当前递增子序列为 1, 3, 4
,在后面加上 5
显然比加个 10000
更 “有希望” 继续增长下去。
这实际上也就是一种贪心算法的思路。据此,我们维护一个数组 d[i]
,表示:长度为 i
的最长递增子序列的末尾元素的最小值 ;用 len
记录当前得到的最长递增子序列的长度。显然,初始状态下 d[1] = nums[0]
, len = 1
。
然后,我们遍历 nums
数组:
- 如果
nums[i] > d[len]
,即当前遍历到的数字 大于 当前最长的递增子序列的最小末尾值,我们自然可以将其添加到末尾并加长len
,即d[++len] = nums[i]
。 - 否则,在
d
数组中找到一个d[j]
,满足d[j - 1] < nums[i] < d[j]
;由于我们是顺序遍历nums
,所以长度为j
的最长子序列的最小末尾元素d[j]
此时需要更新为nums[i]
。
由于 d
数组也是单调递增的,上面的第二种情况可以用二分查找快速找到待更新的 d[j]
。
关于
d
数组的单调性,LeetCode官方题解的证明如下(反证法):如果
d[j] ≥ d[i]
且j < i
:我们考虑从长度为
i
的递增子序列的末尾删除i − j
个元素,那么这个序列长度变为j
,且第j
个元素x
(末尾元素)必然小于d[i]
,也就小于d[j]
。那么我们就找到了一个长度为
j
的递增子序列,并且末尾元素x
比d[j]
小,从而产生了矛盾。💡 这与 “
d[j]
表示长度为j
的递增子序列的最小末尾元素” 矛盾。因此数组 d 的单调性得证。
最后的 len
即为所求。
代码(C++)
int lengthOfLIS(vector<int> &nums)
{
int len = 1;
vector<int> d(nums.size() + 1, 0);
d[1] = nums[0];
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] > d[len])
d[++len] = nums[i];
else {
// 二分查找:待更新的位置下标j,即d中第一个大于nums[i]的位置
int left = 1;
int right = len;
int j = 1; // 如果找不到,说明d中所有数都比nums[i]大,最后应更新d[1]
while (left <= right) {
int mid = left + (right - left) / 2;
if (d[mid] < nums[i]) {
j = mid + 1;
left = mid + 1;
} else
right = mid - 1;
}
d[j] = nums[i];
}
}
return len;
}
遍历 nums
的时间复杂度为
O
(
n
)
O(n)
O(n) ,二分查找的时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn) ,因此该算法的时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 。
拓展
在LeetCode题解区还看到一位大哥对本题的深入和延申解法,包括分层 DAG 建模等方法,感觉也很 🐮 🍺 ,参见这篇 最长递增子序列(nlogn 二分法、DAG 模型 和 延伸问题) | 春水煎茶 (writings.sh) 。
674. 最长连续递增序列
题目描述
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l
和 r
(l < r
)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 104
-109 <= nums[i] <= 109
题解
贪心算法秒了:递增的就添加到末尾,否则从1开始重新记录长度。很简单。
int findLengthOfLCIS(vector<int>& nums) {
int length = 1, maxLength = 1;
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] > nums[i - 1])
length++;
else {
maxLength = max(maxLength, length);
length = 1;
}
}
return max(length, maxLength);
}
718. 最长重复子数组
题目描述
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100
题解
动态规划解决。这里的 “子数组” 其实就是 “连续子序列” ,因此参考最长子序列问题的套路:
-
dp
数组的含义:- 记
A
A
A 为:
nums1
中长度为i - 1
的(前缀)子数组 - 记
B
B
B 为:
nums2
中长度为j - 1
的(前缀)子数组
dp[i][j]
表示: A A A 和 B B B 的最长公共子数组的长度这里长度分别为
i - 1
、j - 1
而非i
、j
,是为了方便dp
的初始化(在后续代码中不难看出),否则还需要单独对dp
起始元素进行定义。 - 记
A
A
A 为:
-
状态转移方程:如果
nums1[i - 1] == nums2[j - 1]
,即 A A A 和 B B B 的末尾元素相同,我们就可以把该公共元素加入到之前的公共子数组中:dp[i][j] = dp[i - 1][j - 1] + 1
,并相应地更新最长公共子数组长度:len = max(len, dp[i][j])
。
代码(C++)
int findLength(vector<int> &nums1, vector<int> &nums2)
{
int len = 0;
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
for (int i = 1; i <= nums1.size(); ++i) {
for (int j = 1; j <= nums2.size(); ++j) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
len = max(len, dp[i][j]);
}
}
}
return len;
}
Python
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
dp = [[0] * (len(nums2) + 1) for _ in range(len(nums1) + 1)]
length = 0
for i in range(1, len(nums1) + 1):
for j in range(1, len(nums2) + 1):
if nums1[i - 1] == nums2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
length = max(length, dp[i][j])
return length
Golang
func findLength(nums1 []int, nums2 []int) int {
length := 0
d := make([][]int, len(nums1)+1)
for i := range d {
d[i] = make([]int, len(nums2)+1)
}
for i := 1; i <= len(nums1); i++ {
for j := 1; j <= len(nums2); j++ {
if nums1[i-1] == nums2[j-1] {
d[i][j] = d[i-1][j-1] + 1
length = max(length, d[i][j])
}
}
}
return length
}