前文详细讲解了「动态规划解题套路框架」详情可见 动态规划解题套路框架
在这篇文章中首先介绍动态规划的三个特点,即:存在「重叠子问题」、具备「最优子结构」、正确的「状态转移方程」
其次给出了一个动态规划的框架,即:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义
题目详情可见 最长递增子序列
下面我们结合「三个特点」和「动态规划框架」来详细分析这个问题
分析:原问题「最长严格递增子序列的长度」,我们把原问题稍微转换一下,「以nums[i]
结尾的最长严格递增子序列的长度」
对于一个长度为n
的数组来说,如果我们要计算以nums[n - 1]
结尾的最长严格递增子序列的长度,我们只需要在以nums[0]...nums[n - 2]
结尾的最长严格递增子序列中找到尾数比nums[n - 1]
小的子序列,然后在该数量上 +1 即可,最后选出最大值即为以nums[n - 1]
结尾的最长严格递增子序列的长度
举个简单的例子,对于:nums = {10, 9, 2, 5, 3, 7, 101, 18}
的数组来说,先看下图:
假设0 - 6
的结果都已经计算出来了,现在需要计算以18
结尾的最长递增序列长度。按照我们上面的分析,18
和nums[0]...nums[6]
相比,只有10, 9, 2, 5, 3, 7
比18
小,所以18
可以接在比它小的元素后面形成长度 +1 的递增子序列,我们只需要在长度 +1 的递增子序列中挑选出最长的即为以18
结尾的最长递增子序列的长度
有了上述的分析,我们现在把「动态规划框架」中四个要素明确一下:
n
的值需要根据之前所有元素[0, n-1]
的值来确定,所以区间会不断地向 base case 靠近,所以唯一的「状态」就是数组区间nums[i]
结尾的最长递增序列长度」就是「选择」(每次都在集合中选择符合要求的长度)dp
数组,一般来说数组的下标就是状态转移中会变化的量,即:「状态-区间长度」;数组的值就是以nums[i]
结尾的最长递增序列的长度根据上面的分析,我们可以很容易的写出完整代码:
xxxxxxxxxx
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
// 记录最终答案,res 为 dp[] 中的最大值
int res = 0;
// base case
Arrays.fill(dp, 1);
// 自底向上依次计算 dp[i] 的值
for (int i = 0; i < nums.length; i++) {
// 根据区间 [0, i) 来确定 dp[i] 的值
for (int j = 0; j < i; j++) {
// 说明 nums[i] 可以接到以 nums[j] 结尾的子序列上
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 完成 dp[i] 的计算
// 更新 res
res = Math.max(res, dp[i]);
}
return res;
}
根据上面的代码,很容易给出这个问题的「状态转移方程」:
至此我们已经分析完了,但是我们并没有说这个问题是否存在「重叠子问题」、具备「最优子结构」,我们都用动规完整写出来了,你说有没有呢!!!哈哈哈哈哈
不过呢,我还是准备从这两个方面分析一下
先来说说是否具备「最优子结构」?思考一个很简单的问题,如果我们计算出了{1, 4, 3, 4}
的最优解,现在我们增加一个数字5
,我们之前计算的结果是否可以重用!?根据上面的分析,显然是可以的,因为数字之间不存在相互制约,所以这个问题是具备「最优子结构」的
然后再来说说是否存在「重叠子问题」?关于这个问题,最简单明了的方法就是画出递归树。不说废话,直接看图叭!
至此,这道题所有问题都解决了,时间复杂度
dp
数组的定义。这一步对于任何动态规划问题都很重要,如果不得当或者不够清晰,会阻碍之后的步骤dp
数组的定义,运用数学归纳法的思想,假设dp[0...i-1]
都已知,想办法求出dp[i]
,一旦这一步完成,整个题目基本就解决了但如果无法完成这一步,很可能就是dp
数组的定义不够恰当,需要重新定义dp
数组的含义;或者可能是dp
数组存储的信息还不够,不足以推出下一步的答案,需要把dp
数组扩大成二维数组甚至三维数组
二分搜索详情可见 二分搜索
这种方法就不展开赘述了,有兴趣的可自行搜索,反正自己是想不到这种方法滴滴滴!!!
xxxxxxxxxx
public int lengthOfLIS(int[] nums) {
// 每堆顶部扑克
int[] top = new int[nums.length];
// 扑克堆的数量
int piles = 0;
for (int i = 0; i < nums.length; i++) {
// 要处理的当前扑克
int poker = nums[i];
// 注意区间开闭 [left, right)
int left = 0, right = piles;
while (left < right) {
int mid = left - (left - right) / 2;
if (top[mid] > poker) right = mid;
else if (top[mid] < poker) left = mid + 1;
else right = mid;
}
if (left == piles) piles++;
top[left] = poker;
}
return piles;
}
题目详情可见 俄罗斯套娃信封问题
这就是一个二维的最长递增子序列问题!
第一版思路:先对信封先按照宽度递增排序,如果宽度相等,再按照高度递增排列
注意:这里对宽和高都是递增排序
第二版思路:先对信封先按照宽度递增排序,如果宽度相等,再按照高度递减排列
这样做的一个好处就是,我们可以只用根据高度来计算最长递增子序列。如图:
代码如下:
xxxxxxxxxx
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes, (o1, o2) -> {
if (o1[0] != o2[0]) {
return o1[0] - o2[0];
} else {
return o2[1] - o1[1];
}
});
int[] dp = new int[envelopes.length];
Arrays.fill(dp, 1);
int res = 0;
for (int i = 0; i < envelopes.length; i++) {
for (int j = 0; j < i; j++) {
if (envelopes[i][1] > envelopes[j][1]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
很遗憾,超时了!!😭😭😭
第三版思路:利用二分搜索
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes, (o1, o2) -> {
if (o1[0] != o2[0]) {
return o1[0] - o2[0];
} else {
return o2[1] - o1[1];
}
});
int n = envelopes.length;
int[] top = new int[n];
int piles = 0;
for (int i = 0; i < n; i++) {
int poker = envelopes[i][1];
int left = 0, right = piles;
while (left < right) {
int mid = left - (left - right) / 2;
if (top[mid] < poker) left = mid + 1;
else if (top[mid] > poker) right = mid;
else right = mid;
}
if (left == piles) piles++;
top[left] = poker;
}
return piles;
}