Offer 驾到,掘友接招!我正在参与2022春招系列活动-刷题打卡任务活动详情
一、题目描述
给定一排人的身高,求每个人往右边看过去,能看到多少人?
能看到对方,当且仅当两个人中间的所有人都比这两个人矮
数据范围
n <= 1e5
, h[i] <= 1e5
二、思路分析
朴素想法: 每个人的右边能看到的人数,那直接暴力往右边扫过去,取一个单调递增的序列,序列的长度就是这个人能看到的人的数量,但是这样复杂度有点高,我们开始思考应该从哪降低复杂度,我们发现,从左往右对每个人做一次“暴力往右扫”这件事,有点浪费时间,如果反过来做,并不影响每个人的答案,且能复用上一个人的结果,也就是说,从右往左做的话是没有后效性的。
不朴素的想法: 从右往左做,不断维持一个递减的单调栈,对于当前的i
,它能看到的就是序列中所有比它小的数和离它最近的大于它的第一个数 (注意,这里将会在代码中产生一处特判)。
例如下图,当前的红色i能看到的其实就是右边那四个块,对此我们用一个upper_bound
就可以了。
注意,这里的单调栈是递减的,所以该函数需要重载运算符
int k = upper_bound(q.begin(), q.end(), heights[i], greater<int>() )-q.begin();
接下来,我们要把当前的h[i]
入栈,显而易见的是,从栈尾开始的那几个“矮子”将被挡住,以后不会产生贡献了,所以可以踢出去。
最后边做边算答案就是了。
更不朴素的想法: 但是我们发现上面的做法有一点傻的地方,我们为什么用二分查找?因为我们要找到所有比当前的h[i]
小的数,查完了后我们做了什么?为了把h[i]
入栈,把所有比h[i]
小的数弹出去......这其实就是同一件事,也就是说,咱根本没有必要用二分查找,弹几个数,答案就是几个了。改完后复杂度从O(nlogn)
变成了O(n)
三、AC代码
//一开始的O(nlogn)做法
class Solution {
public:
vector<int > q,ans;
vector<int> canSeePersonsCount(vector<int>& heights) {
for (int i=heights.size()-1; i>=0; i--) {
int k = upper_bound(q.begin(), q.end(), heights[i], greater<int>() )-q.begin();
//特判一下,如果整个队列都比当前小,那么就不存在那个“第一个比自己大”的人
ans.push_back(k? q.size() - k + 1 : q.size());
while (!q.empty() && heights[i]>=q.back()) q.pop_back();
q.push_back(heights[i]);
}
int n = ans.size();
for (int i=0; i<n/2; i++) swap(ans[i],ans[n-i-1]);
return ans;
}
};
//改进后的O(n)做法
class Solution {
public:
vector<int > q,ans;
vector<int> canSeePersonsCount(vector<int>& heights) {
int hs = heights.size();
for (int i=hs-1; i>=0; i--) {
int count = 0;
while (!q.empty() && heights[i]>=q.back()) {
q.pop_back();
count++;
}
if (!q.empty()) count++;
ans.push_back(count);
q.push_back(heights[i]);
}
int n = ans.size();
for (int i=0; i<n/2; i++) swap(ans[i],ans[n-i-1]);
return ans;
}
};
四、总结
基于我们所观察到的每个人看到的右边的人是单调递增的,故利用单调栈去维护“可看见的人”的序列,是一道很好的单调栈题目。