俄罗斯套娃信信封问题
给定一些标记了宽度和高度的信封,宽度和高度以 整数对 的形式(w, h)
出现。当另一个信奉的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里吗,如同俄罗斯套娃。 请计算最多能有多少个信奉能组成一组“俄罗斯套娃”信封(不允许旋转信封)
示例:
输入: envelopes = [[5, 4],[6, 4],[6, 4],[2, 3] ]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
解析:
该问题需要先按特定的规则进行排序,排序后就转换成为一个最长递增子序列问题
可以先看下面这个题目:
最长递增子序列:
给一个整数数组nums
,找到其中最长严格递增子序列长度。子序列是由数组派生而来的的序列,删除(或不删除)数组中的元素而不改变其余元素顺序。例如,[3,6,2,7]
是数组[0,3,1,6,2,2,7]
的子序列。
- 注意题中所说的是【子序列】和【子串】是不一样的,子串一定是连续的,但子序列不一定是连续的
动态规划解法:
动态规划的核心即数学归纳法,主要思路就是:若想证明一个结论是否成立,先假设这个结论在k<n
是成立,然后根据这个假设,想办法证明k=n
时结论也成立。如果能够证明出来,那么说明这个结论对于k
等于任何数都成立。类似的在设计动态规划,需要一个dp数组,假设dp[0,..,i-1]
已经计算出来,如何通过这些结果计算dp[i]
- 在此题中,定义:
dp[i]
表示以nums[i]
这个数结尾的最长递增子序列的长度
根据这个定义,dp[i]
的初始值至少要为1,具体过程如下:
根据这个定义,子序列的最大长度应该是dp数组中的最大值
int ret = 0;
for(int i = 0; i < dp.length; i++){
ret = Math.max(dp[0], dp[dp.length - 1]);
}
return ret;
这个dp数组是我们用眼睛看出来的,如何用逻辑实现计算每个dp[i]?使用数学归纳思想:假设我们已经知道了dp[0,..,4]
的所有结果,如何推出dp[5]
?
根据之前对dp数组的定义,dp[5]
的含义就是以nums[5]
为结尾的最长递增子序列的长度
此处nums[5] = 3
,又因为要找的是递增子序列,只需要找到前面那些结尾比3小的子序列,然后把3接到这个子序列的后面,就得到一个新的递增的序列,且这个新子序列的长度加一
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
i=5这个位置,j遍历nums数组,
因为是要得到递增的子序列,我们用nums[i]这个位置的值与nums[j]这个位置的值进行比较
小了,那就不是该放入递增子序列的数,j++
大了,就是递增子序列,举几个例子:
j = 0, i = 5; nums[i] > nums[j]; dp[5] = Math.max{d[5], dp[0] + 1} = Math.max{0, 1 + 1} = 2
j = 4, i = 5; nums[i] > nums[j]; dp[5] = Math.max{d[5], dp[4] + 1} = Math.max{2, 2 + 1} = 2
求出了dp[5]
那么前面的类似于数学归纳法也可以计算
for(int i = 0; i < nums.length; i++){
for(int j=0; j < i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
}
完整代码如下:
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
// base case:dp 数组全都初始化为 1
Arrays.fill(dp, 1); //Arrays.fill() 将dp数组中每个位置填充1
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
int res = 0;
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
- 总结如何找到动态规划的状态转移关系:
1、明确dp
数组所存数据的含义。
2、根据dp
数组的定义,运用数学归纳法的思想,假设dp[0,...,i-1]
都已知,想办法求出dp[i]
,如果无法求出,可能是dp
数组定义不够恰当或dp
数组存储的信息还不够,将其扩大到二维甚至三维。
此题还有二分查找解法,因为俄罗斯套娃信封问题用不到,所以不做说明。
现在回到信封嵌套问题:
这其实是最长递增子序列(LIS)的一个变种,很明显,每次合法的嵌套是大的套小的,相当于找一个最长递增子序列,其长度就是最多能嵌套的信封个数,但问题在于,此处的信封是二维的。
解法:
现对宽度w
进行升序排列,如果遇到w
相同的情况,则按照高度h
降序排序。之后把所有的h
作为一个数组,在这个数组上计算LIS的长度就是答案。
- 现队这些数对按宽度进行升序排序
- 然后再
h
上寻找最长递增子序列:
- 对于宽度
w
相同的数对,对其高度h
进行降序排序。因为两个宽度相同的信封不能互相包含,逆序排序保证在w
相同的数对中最多只选取一个。
// envelopes = [[w, h], [w, h], ...]
public int maxEnvelopes(int[][] envelopes){
int n = envelopes.length;
// 按宽度升序排列,如果宽度一样,则按高度降序排列
Arrays.sort(envelopes, new Comparator<int[]>()
{
@Override
public int compare(int[] a, int[] b) {
//a[i]里面的i指的是按照每一行的第i列进行排序
// 所以a[0]就是把按照第0列的那个数对所有行进行排序,b[0]既代表其他行的数字
// a[1]就是按照第一列进行排序。
if(a[0]==b[0]){
return b[1]-a[1]; //如果第 0列的数字相等,那么按第 1列的降序
}else{
return a[0]-b[0]; //如果第 0列的数字不等,那么按第 0列的升序
}
}
});
// 对高度数组寻找LIS
int[] height = new int[n];
for(int i = 0; i< n; i++){
height[i] = envelopes[i][1]; //将高度这一部分放进一个新的数组
}
return lengthOfLIS(height);
}
- 是用动态规划设计最长递增子序列
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
// base case:dp 数组全都初始化为 1
Arrays.fill(dp, 1); //Arrays.fill() 将dp数组中每个位置填充1
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
int res = 0;
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
该题目解析均来自 —— labuladong