问题描述
小F在“双十一”期间购买了N件商品。每件商品有一个价格p[i],小F可以获得的优惠取决于该商品之前的一件商品。如果某一件商品的价格p[i]大于等于前面的某个商品p[j],则小F可以享受该商品p[j]的价格作为优惠,前提是p[j]是离p[i]最近的且满足条件的商品。
例如,给定价格数组p = [9, 4, 5, 2, 4],其中p[3] = 2之前没有商品的价格小于等于p[3],因此没有优惠;而p[2] = 5可以享受最近的商品p[1] = 4的价格作为优惠。因此,任务是计算小F能获得的总优惠。
测试样例
样例1:
输入:
N = 5 ,p = [9, 4, 5, 2, 4]
输出:6
样例2:
输入:
N = 4 ,p = [1, 2, 3, 5]
输出:6
样例3:
输入:
N = 4 ,p = [4, 3, 2, 1]
输出:0
解题思路
采用单调递增栈:
-
栈中的元素始终保持递增(从栈底到栈顶)。
-
遍历数组,对于每个 p[i]:
- 如果栈顶元素大于 p[i],弹出栈,保持栈的单调性。
- 如果栈顶元素小于等于 p[i],它就是左侧最近的可用商品价格,加入总优惠。
- 将 p[i] 压入栈。
代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
const int M = 100010;
int st[M], tt;
int solution(int N, std::vector<int>& p) {
tt = 0;
st[++tt] = p[0];
int total = 0;
for(int i = 1; i < N; i++) {
while(tt != 0 && p[i] < st[tt]) {
tt--;
}
if (tt != 0)
total += st[tt];
st[++tt] = p[i];
}
return total;
}
int main() {
std::vector<int> p1 = {9, 4, 5, 2, 4};
std::cout << (solution(5, p1) == 6) << std::endl;
std::vector<int> p2 = {1, 2, 3, 5};
std::cout << (solution(4, p2) == 6) << std::endl;
std::vector<int> p3 = {4, 3, 2, 1};
std::cout << (solution(4, p3) == 0) << std::endl;
return 0;
}
关键代码解释
-
栈初始化与压栈:
st[++tt] = p[0];将第一个商品价格直接入栈作为基准。
-
弹栈逻辑:
while (tt != 0 && p[i] < st[tt]) { tt--; }当前商品价格 p[i] 小于栈顶元素时,栈顶元素不能作为 p[i] 的优惠商品,弹出栈。
-
优惠计算:
if (tt != 0) { total += st[tt]; }如果栈不为空,栈顶元素是 p[i] 的优惠商品,加入总优惠。
-
压入当前商品:
st[++tt] = p[i];将当前商品价格入栈,保持单调递增。
时间空间复杂度分析
-
每个元素入栈和出栈至多一次:
每个元素 p[i] 在整个过程中只会入栈一次,弹出栈一次。- 入栈操作: O(N)
- 出栈操作: O(N)
-
时间总复杂度: O(N)。
-
空间复杂度: 使用额外的栈存储商品价格,空间复杂度为 O(N)O(N)O(N)。
学习思考
单调栈是一种特殊的栈结构,栈内的元素根据一定规则保持单调性:
- 单调递增栈: 从栈底到栈顶,元素依次递增(或非递减)。
- 单调递减栈: 从栈底到栈顶,元素依次递减(或非递增)。
在遍历的过程中,通过栈的出栈和入栈操作,动态维护这一单调性,从而高效地解决一类问题。
常见应用场景
-
找数组中左/右侧最近满足条件的元素:
- 问题描述: 给定一个数组,找到每个元素左侧或右侧最近的满足大小关系的元素。
- 例子: 柱状图中找到每根柱子左/右侧第一个比它矮的柱子。
-
滑动窗口的最大值/最小值问题:
- 问题描述: 找到滑动窗口中最大或最小值。
- 例子: 单调栈可以动态维护窗口中元素的大小关系。
-
区间问题:
- 通过单调栈高效计算区间最大值、最小值及其贡献。
- 例子: 求每个元素作为区间最大/最小值的贡献。
-
经典题目:
- 接雨水问题: 通过单调栈计算两根柱子之间的水量。
- 最大矩形问题: 通过单调栈找直方图中的最大矩形面积。
学习单调栈的关键点
-
理解单调性维护的逻辑:
- 当栈顶元素不再满足单调性时,需要弹出元素。
- 为什么弹出?因为当前元素更接近目标,同时满足单调性。
- 例子: 单调递增栈中,遇到较小的元素,说明之前的元素不再对后续有用。
-
明确出栈操作的含义:
- 每次弹出栈顶元素时,意味着当前元素对栈顶元素形成了某种匹配。
- 例子: 在找左侧最近更小元素时,出栈意味着找到更小的约束。
-
栈内存储的数据结构:
- 栈中可以不仅存元素本身,也可以存元素的索引等额外信息,便于回溯或计算区间长度。
- 例子: 找到区间贡献时,栈存索引更合适。
-
结合具体问题调试单调栈:
- 对于每次入栈和出栈操作,追踪栈的状态变化。
- 思考每个弹出元素如何参与结果的计算。