青训营X豆包MarsCode 技术训练营:价格优惠计算问题c++题解 | 豆包MarsCode AI 刷题

108 阅读4分钟

问题描述

小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

解题思路

采用单调递增栈:

  1. 栈中的元素始终保持递增(从栈底到栈顶)。

  2. 遍历数组,对于每个 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;
}

关键代码解释

  1. 栈初始化与压栈:

    st[++tt] = p[0];
    

    将第一个商品价格直接入栈作为基准。

  2. 弹栈逻辑:

    while (tt != 0 && p[i] < st[tt]) {
        tt--;
    }
    

    当前商品价格 p[i] 小于栈顶元素时,栈顶元素不能作为 p[i] 的优惠商品,弹出栈。

  3. 优惠计算:

    if (tt != 0) {
        total += st[tt];
    }
    

    如果栈不为空,栈顶元素是 p[i] 的优惠商品,加入总优惠。

  4. 压入当前商品:

    st[++tt] = p[i];
    

    将当前商品价格入栈,保持单调递增。

时间空间复杂度分析

  • 每个元素入栈和出栈至多一次:
    每个元素 p[i] 在整个过程中只会入栈一次,弹出栈一次。

    • 入栈操作: O(N)
    • 出栈操作: O(N)
  • 时间总复杂度: O(N)。

  • 空间复杂度: 使用额外的栈存储商品价格,空间复杂度为 O(N)O(N)O(N)。

学习思考

单调栈是一种特殊的栈结构,栈内的元素根据一定规则保持单调性:

  • 单调递增栈: 从栈底到栈顶,元素依次递增(或非递减)。
  • 单调递减栈: 从栈底到栈顶,元素依次递减(或非递增)。

在遍历的过程中,通过栈的出栈和入栈操作,动态维护这一单调性,从而高效地解决一类问题。

常见应用场景

  1. 找数组中左/右侧最近满足条件的元素:

    • 问题描述: 给定一个数组,找到每个元素左侧或右侧最近的满足大小关系的元素。
    • 例子: 柱状图中找到每根柱子左/右侧第一个比它矮的柱子。
  2. 滑动窗口的最大值/最小值问题:

    • 问题描述: 找到滑动窗口中最大或最小值。
    • 例子: 单调栈可以动态维护窗口中元素的大小关系。
  3. 区间问题:

    • 通过单调栈高效计算区间最大值、最小值及其贡献。
    • 例子: 求每个元素作为区间最大/最小值的贡献。
  4. 经典题目:

    • 接雨水问题: 通过单调栈计算两根柱子之间的水量。
    • 最大矩形问题: 通过单调栈找直方图中的最大矩形面积。

学习单调栈的关键点

  1. 理解单调性维护的逻辑:

    • 当栈顶元素不再满足单调性时,需要弹出元素。
    • 为什么弹出?因为当前元素更接近目标,同时满足单调性。
    • 例子: 单调递增栈中,遇到较小的元素,说明之前的元素不再对后续有用。
  2. 明确出栈操作的含义:

    • 每次弹出栈顶元素时,意味着当前元素对栈顶元素形成了某种匹配。
    • 例子: 在找左侧最近更小元素时,出栈意味着找到更小的约束。
  3. 栈内存储的数据结构:

    • 栈中可以不仅存元素本身,也可以存元素的索引等额外信息,便于回溯或计算区间长度。
    • 例子: 找到区间贡献时,栈存索引更合适。
  4. 结合具体问题调试单调栈:

    • 对于每次入栈和出栈操作,追踪栈的状态变化。
    • 思考每个弹出元素如何参与结果的计算。