LeetCode刷题—动态规划(二)
刷了一系列这类的题,真的感受到dp 深深的套路,那就看看 什么套路来解此类题吧!
300,最长递增子序列,medium 673,最长递增子序列的个数,medium 435,无重叠区间,medium 646,最长数对链,medium 452,用最少数量的箭引爆气球,medium
最长递增子序列
最长递增子序列(Longest Increasing Subsequence,简写 LIS)是比较经典的一个问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2)。注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。
300,最长递增子序列,medium
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18] |
示例 2:
输入:nums = [0,1,0,3,2,3] |
示例 3:
输入:nums = [7,7,7,7,7,7,7] |
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
思路:
子问题
到 nums[i] 的最长递增子序列的长度为 \(dp[i]\)
递推关系
base case
dp 数组应全部初始化为1,因为子序列最少也要包含自己。
状态转移方程
在这里插入图片描述 对于 \(dp[i]\),比较 nums[i] 与前 i - 1 个元素,如果有比 nums[i] 小的 元素 nums[j],则 nums[i] 可以加入已形成的 \(dp[j]\) 之后,\(dp[i]\) 为与 \(dp[j] + 1\) 取最大值后的结果。
返回值
最终返回最大的 \(dp[i]\),在第一次遍历数组得到 \(dp[0]\) ~ \(dp[i-1]\) 时,不断取最大值更新 res。
代码:
#### 673,最长递增子序列的个数,mediumclass Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int res = 1;
int[] dp = new int[n];
//base case
Arrays.fill(dp, 1);
//得到 dp[0] ~ dp[i-1]
for(int i = 0; i < n; i++){
//nums[i] 与前 i-1 个数字进行比较,得到 dp[i]
for(int j = 0; j < i; j++){
if(nums[j] < nums[i])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
//在dp[0]~dp[n-1] 中找出最大值即为结果
res = Math.max(res, dp[i]);
}
return res;
}
}
给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 1:
输入: [1,3,5,4,7] |
示例 2:
输入: [2,2,2,2,2] |
思路:
子问题
到 nums[i] 结尾,最长递增子序列的长度为 \(dp[i]\),此外还需要一个数组 \(count[i]\) 来记录具有该长度的序列个数。
递推关系
base case
dp 数组应全部初始化为1,因为子序列最少也要包含自己。
count 数组全部初始化为1,个数至少有一个。
状态转移方程
遍历[0...i],再套一层[0....j],其中 j<i
当
nums[j]<nums[i]
,说明可以形成最长递增序列,nums[i] 可以加入已形成的 \(dp[j]\) 之后。- 当 \(dp[j] + 1 > dp[i]\) 时,第一次出现此长度,\(dp[i] = dp[j] + 1\),最长递增子序列的长度增加 但 个数不变,count[i] = count[j]
- 当 \(dp[j] + 1 = dp[i]\) 时,在循环中已经出现过此长度,现在的组合方式
count[i]
加上count[j]
并记录最大递增子序列长度
返回值
返回最大长度对应的种类数之和。
举例:
输入:[1,1,3,2]
代码:
class Solution {
public int findNumberOfLIS(int[] nums) {
if(nums.length == 0) return 0;
int n = nums.length;
int res = 0;
int maxLength = 0;
int[] dp = new int[n];
int[] count = new int[n];
Arrays.fill(dp, 1);
Arrays.fill(count, 1);
for(int i = 0; i < n; i++){
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
if(dp[j] + 1 > dp[i]){
dp[i] = dp[j] + 1;
count[i] = count[j];
}
else if(dp[j] + 1 == dp[i]){
count[i] += count[j];
}
}
}
maxLength = Math.max(maxLength, dp[i]);
}
for(int i = 0; i < n; i++){
if(dp[i] == maxLength){
res += count[i];
}
return res;
}
}
一个小总结
- 上面两题思路是很像的,动态规划基于数学归纳法。
- 先得到第 i 位 的 最长递增子序列个数,最终结果(子序列的最大长度)应该是 dp 数组中的最大值。
- 而对 dp[i],与前面的 i - 1 个数进行比较,加在前面的递增子序列之后,不断取最大值,得到 dp[i]。
- 第二题 多增加了count 数组来记录最长子序列的个数,注意循环中的比较。
下面又是【最长递增子序列】的另一种题型,将上面题中的数组划分很多个小区间,如果能识别是同一种题就好解决了!
435,无重叠区间,medium
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
- 可以认为区间的终点总是大于它的起点。
- 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ] |
示例 2:
输入: [ [1,2], [1,2], [1,2] ] |
示例 3:
输入: [ [1,2], [2,3] ] |
问题解析:
可以抽象成上面的问题:最长非严格递增子序列。题目要求可以接触但不可以重叠,即非严格递增。子序列长度改为区间个数,【删除区间的最小数量】即 【保留区间的最大个数】。
思路:
子问题
到第 i 个区间,保留的最大区间数为 \(dp[i]\)。
递推关系
base case
dp 数组应全部初始化为1,因为最少也要保留一个区间。
状态转移方程
对于 \(dp[i]\),前面 j = [0, i - 1] 个区间都有可能与它重叠(重叠即 j 的第 1 个元素 > i 的第 0 个元素),需要得到保留不重叠的区间的最大值。
\(dp[i] = Math.max(dp[i], dp[j] + 1)\)
返回值
遍历过程中得到 ans 即 保留不重叠的区间的最大值,最后返回 n - ans,即删除的最小区间个数。
- 代码:
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if(intervals.length == 0) return 0;
int n = intervals.length;
//对区间按起始数字进行排序
Arrays.sort(intervals, new Comparator<int[]>(){
public int compare(int[] o1, int[] o2) {
return o1[1]-o2[1];
}
});
//保留的最大区间个数
int ans = 1;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 1; i < n; i++){
for(int j = 0; j < i; j++){
//如果没有重叠(即右边的0元素大于等于左边的1元素),更新dp[i]
if(intervals[i][0] >= intervals[j][1])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
ans = Math.max(ans, dp[i]);
}
//删除的最小数量
return n - ans;
}
} 另一种:双指针
思路:
先对各个区间从小到大排序,左区间小的优先;相同情况下,右区间小的优先。
排序后两个指针分别指向相邻两个区间,不发生重叠继续比较下两个相邻区间;发生重叠则去除 右元素较大的一个区间,这样较小的右区间才会尽可能减小与其他区间交集。
代码:
#### 646,最长数对链,mediumclass Solution {
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, new Comparator<int[]>() {
public int compare(int[] o1, int[] o2) {
if (o1[0] != o2[0]) {
return o1[0] - o2[0];
}else
return o1[1] - o2[1];
}
});
int count = 0;
//i指向待比较的左边区间,j指向待比较的右边区间
for (int i = 0, j = 1; i < intervals.length-1 && j<=intervals.length-1; i++,j++) {
//左边的1元素 > 右边的0元素,发生重叠,需要去除其中一个
if (intervals[i][1] > intervals[j][0]) {
//去除1元素较大的那个,如果右边区间的1元素较大,保留左边,i = i - 1,再次进入循环 i + 1,还在原位置;
//如果左边区间的1元素较大,保留右边,i = j - 1,再次进入循环 i + 1,i = j , j向后移
i = intervals[i][1] < intervals[j][1] ? i - 1: j - 1;
count++;
}
//不发生重叠
else {
i = j - 1;
}
}
return count;
}
}
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
示例:
输入:[[1,2], [2,3], [3,4]] |
- 问题说明:与上一题的区别在于数对之间不可以有相等数字,是严格递增的区间。返回的是最长数对区间个数。
- 代码: #### 452,用最少数量的箭引爆气球,medium
class Solution {
public int findLongestChain(int[][] pairs) {
int n = pairs.length;
Arrays.sort(paris, new Comparator<int[]>(){
public int compare(int[] o1, int[] o2) {
if (o1[0] != o2[0]) {
return o1[0] - o2[0];
}else
return o1[1] - o2[1];
}
});
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 1; i < n; i++){
for(int j = 0; j < i; j++){
//不发生重叠, 更新 dp[i]
if(pairs[i][0] > pairs[j][1]){
dp[i] = dp[j] + 1;
}
}
}
//上面对pairs 进行了排序,dp数组递增,返回最后一个元素即最大值
return dp[n - 1];
}
}
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart
,xend
, 且满足 xstart ≤ x ≤ xend
,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points
,其中 points [i] = [xstart,xend]
,返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]] |
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]] |
- 问题说明:\(dp[i]\) 表示 到第 i 个区间的最小弓箭数。如果 左边区间的1元素 >= 右边区间的0元素,说明有重叠,可以共用一个箭;如果 左边区间的1元素 < 右边区间的0元素,没有重叠,\(dp[i] = dp[j] + 1\)。
- 代码:
class Solution {
public int findMinArrowShots(int[][] points) {
if(points.length == 0) return 0;
int n = points.length;
Arrays.sort(points, new Comparator<int[]>(){
public int compare(int[] o1, int[] o2) {
if (o1[0] != o2[0]) {
return Integer.compare(o1[0], o2[0]);
}else
return Integer.compare(o1[1], o2[1]);
}
});
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 1; i < n; i++){
for(int j = 0; j <= i - 1; j++){
//如果区间不重叠(左边的1元素 < 右边的0元素),dp[j] + 1
if(points[i][0] > points[j][1])
dp[i] = dp[j] + 1;
}
}
return dp[n - 1];
}
} - 注意:排序时,如果使用
o1[0]-o2[0]
、o1[1]-o2[1]
在 [[-2147483646,-2147483645],[2147483646,2147483647]] 测试用例时会导致整形溢出。
一个小总结
这里动态规划的解法并不高效,只是为了讲解整体思路。
上面几题都是先对区间排序,再根据题意看是否重叠更新dp[i]。
继续刷题!继续更新!