题目:
有个马戏团正在设计叠罗汉的表演节目,一个人要站在另一人的肩膀上。出于实际和美观的考虑,在上面的人要比下面的人矮一点且轻一点。已知马戏团每个人的身高和体重,请编写代码计算叠罗汉最多能叠几个人。
前言:
题目要求是“矮一点且轻一点”,是严格要求<而不是<=;题目要求在2个维度上同时保持严格递增,那么我们可以先将其中一个维度排好序,以保证在一个维度上保持递增(此时并非严格递增),之后就可以专注于处理另一个维度。
具体而言:先根据身高升序排序,若身高一样则根据体重降序排序。身高排序好之后,剩余待处理的就是体重。处理体重问题就是处理最长递增子序列的问题。
为什么身高相同时,体重需要降序排序呢?
其实很简单的道理,将身高相同的人看成1个集合,若他们都按照体重升序排序,则之后的二分法处理中,有一定的概率会在这集合中取>=2个人作为最终结果。
解法一(动态规划):
先将身高进行升序排序,然后对得到结果的体重数组利用动态规划思想进行最长递增子序列问题的计算,如下为实现具体思路。
定义 dp[i] 为考虑前i个元素,以第i个数字结尾的最长上升子序列的长度,注意nums[i]必须被选取。从小到大计算dp数组的值,在计算dp[i]之前,我们已经计算出dp[0...i-1]的值,并得到状态转移方程。其中,动态规划的状态数为n,计算状态dp[i]时,需要O(n)的时间遍历dp[0...i-1]的所有状态,所有总时间复杂度为O(n^2)。空间复杂度为O(n),需要额外使用长度为n的dp数组。时间超限。
解法二(贪心+二分查找法):
在贪心+二分查找法的思路中,考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上那个尽可能的小。如下为实现代码:
class Solution {
public:
int bestSeqAtIndex(vector<int>& height, vector<int>& weight) {
//创建数组,将身高和体重信息作为一个数组对<int,int>均添加到数组tmp中
vector<pair<int,int>> tmp;
for(int i = 0; i < height.size(); i++) tmp.push_back({height[i], weight[i]});
//如果a和b第一个元素相同,则按照降序(a和b的second)进行排序
//如果a和b第一个元素不相同,则按照升序(a和b的first)进行排序
sort(tmp.begin(), tmp.end(), [](const pair<int,int> &a, const pair<int,int> &b) {
return a.first == b.first ? a.second > b.second : a.first < b.first;
});
vector<int> dp; //长度为N的地方 最小的数字
//如果在dp数组中如果w大于等于dp中的任何一个元素,则将其添加到数组末尾中
//否则,用w替换在dp此时的数组中第一个大于等于w本身的元素,保持此时数组递增子序列最大
for(const auto &[h, w]: tmp) {
auto p = lower_bound(dp.begin(), dp.end(), w); //二分查找第一个大于等于的地方
if(p == dp.end()) dp.push_back(w);
else *p = w;
}
return dp.size();
}
};
时间复杂度:O(nlogn)。数组 nums 的长度为 n,我们依次用数组中的元素去更新 d 数组,而更新 d 数组时需要进行 O(logn) 的二分搜索,所以总时间复杂度为 O(nlogn)。空间复杂度:O(n),需要额外使用长度为 n 的 d 数组。
笔者小记:
1、sort()函数自定义排序,以下面为具体例子:
sort(tmp.begin(), tmp.end(), [](const pair<int,int> &a, const pair<int,int> &b)
{return a.first == b.first ? a.second > b.second : a.first < b.first;});
其中(const pair<int,int> &a, const pair<int,int> &b)表示输入比较的两个类型对象【适用于pair<int, int>类型数据的特定排序需求】,a和b名称为假设的,return为排序返回的结果情况,如果a.first==b.first,则a.second>b.second返回,即按照规则得到的降序排列,否则按照a.first<b.first规则得到的升序进行排列。“>”表示按照降序排列的,“<”表示按照升序排列的(a和b的顺序固定)。特别注意const pair<int,int> &a与vector<vector<int>>不相同。
2、std::lower_bound用于在有序序列中查找第一个大于等于目标值的元素的位置。用于在有序序列中查找第一个大于或等于目标值的元素的位置,时间复杂度是O(log n)
(二分查找)。其中可以通过*p对其进行元素的修改赋值操作(例如*p = w),*p即表示元素索引位置对应的值。(可以将auto p 理解为类似指针操作的功能)
auto p = lower_bound(dp.begin(), dp.end(), w)
3、二分查找仅适用于有序数组(单调递增或单调递减),对无序数组不使用,时间复杂度 O(log n),比线性查找高效得多。常见应用:1、基础查找(查找 target
);2、查找 ≥ target
/≤ target
的元素;3、最接近的值;二分答案(求解最优值)。