代码随想录算法训练营Day10 栈与队列

栈与队列

栈与队列的题目我做的比较少,感觉主要的用法是单调栈和单调队列,以此来记录某些内容。至于如何运用,核心是抓住“先进先出”(队列)和“先进后出”(栈)的特性。

想起来终测第一天的时候卡死我,导致有一个单调栈的题目我都没写,后面发现还简单一点。。。这里也一并记录。


Leetcode 232. 用栈实现队列

题目链接:https://leetcode.cn/problems/implement-queue-using-stacks/description/

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true;否则,返回 false

说明:

  • 只能 使用标准的栈操作 —— 也就是说只有 push to toppeek/pop from topsizeis empty 操作是合法的。

  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

题目解析:这个题目纯粹是为了练习而出题。因为栈是先进后出,队列是先进先出,那么我们考虑用两个栈来实现这个操作——其中一个栈(stkIn)用来模拟push,另一个栈(stkOut)处理pop的顺序。

class MyQueue {
public:
    stack<int> stkIn, stkOut;
    MyQueue() {
        
    }
    
    // 无脑进入就行啦
    void push(int x) {
        stkIn.push(x);
    }
    
    // 如果stkOut是空的,说明没有元素要准备出去
    // 我们把stkIn的”所有“元素塞进去
    // 因为这些元素的相对先后顺序不能发生变化
    // 只能当作一块来处理
    int pop() {
        if (stkOut.empty()) {
            while (!stkIn.empty()) {
                stkOut.push(stkIn.top());
                stkIn.pop();
            }
        }
        int res = stkOut.top();
        stkOut.pop();
        return res;
    }
    
    // 直接调用pop函数,然后假装没有pop,把他塞回去。
    int peek() {
        int res = this->pop();
        stkOut.push(res);
        return res;
    }

    // 全部为空才是空
    bool empty() {
        return stkIn.empty() && stkOut.empty();
    }
};

Leetcode 225. 用队列实现栈

题目链接:https://leetcode.cn/problems/implement-stack-using-queues/description/

题目描述:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true;否则,返回 false

注意:

  • 你只能使用队列的标准操作 —— 也就是 push to backpeek/pop from frontsizeis empty 这些操作。

  • 你所使用的语言也许不支持队列。你可以使用 list(列表)或者 deque(双端队列)来模拟一个队列,只要是标准的队列操作即可。

题目解析:用队列实现栈,主要的难点仍然是他们核心的特性。因为队列是“先进先出”,那么当后面插入一个元素的时候,我们需要让他“后来居上”。具体的是实现是:我们获取到“要让几个元素移到后面去”,然后不断pop()push_back()

class MyStack {
public:
    queue<int> q;
    MyStack() {
        
    }
    
    // 只有前面能出去,那么我们就尝试反向排队,后来者居上。
    void push(int x) {
        int n = q.size();
        q.push(x);
        for (int i = 0; i < n; i ++) {
            q.push(q.front());
            q.pop();
        }
    }
    
    int pop() {
        int res = q.front();
        q.pop();
        return res;
    }
    
    int top() {
        return q.front();
    }
    
    bool empty() {
        return q.empty();
    }
};

Leetcode 20. 有效的括号

题目链接:https://leetcode.cn/problems/valid-parentheses/description/

题目描述:给定一个只包括 '('')''{''}''['']' 的字符串 s,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

题目解析:这个题目开始利用栈的性质了。简单来说,栈的性质可以用来“匹配”。如果有某种特性可以用来“匹配”,那么就可以考虑栈。

具体而言,这道题目需要无脑塞左括号,然后遇到匹配的右括号就弹出,因为他已经合法了。这样的原理是,如果合法,那么我们后加入栈的元素一定是先合法,可以形象化描述“先脱单”。

class Solution {
public:
    bool isValid(string s) {
        stack<char> stk;
        auto match = [&](char c) -> char {
            if (c == '(') return ')';
            if (c == '[') return ']';
            if (c == '{') return '}';
            return '0';
        };
        for (auto& c : s) {
            if (stk.empty()) {
                stk.push(c);
                continue;
            }
            if (c == match(stk.top())) {
                stk.pop();
            } else {
                stk.push(c);
            }
        }
        return stk.empty();
    }
};

Leetcode 1047. 删除字符串中的所有相邻重复项

题目链接:https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/description/

题目描述:给定一个只包括 '('')','{''}''['']'的字符串s`,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

题目解析:这个题目和上个题目一样,只是“脱单条件”变成了“相同”。

class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> stk;
        for (auto& c : s) {
            if (stk.empty()) {
                stk.push(c);
                continue;
            }
            if (c == stk.top()) {
                stk.pop();
            } else {
                stk.push(c);
            }
        }
        string res = "";
        while (stk.size()) {
            res += stk.top();
            stk.pop();
        }
        ranges::reverse(res);
        return res;
    }
};

但是这个代码还是太丑了,因为你其实不需要真的定义一个stack,STL中给string提供了类似的接口。

class Solution {
public:
    string removeDuplicates(string s) {
        string stk;
        for (auto& c : s) {
            if (!stk.empty() && stk.back() == c) {
                stk.pop_back();
            } else {
                stk.push_back(c);
            }
        }
        return stk;
    }
};

Codeforces HIT终测第一场D题

题目链接:https://codeforces.com/gym/627958/problem/D

题目描述:nanani 非常爱喝酒,尽管她的酒品不是很好。

朋友们为 nanani 准备了香槟塔,希望她能喝到爽。香槟塔可以简化成这样的模型:

香槟塔一共有 \(n\) 层,从上到下依次被标号为 \(1, 2, 3, \dots, n\),每个香槟塔都有不同的容量 \(a_i\),即最多可以装 \(a_i\) 单位的酒。初始时所有香槟塔都为空。

现在 nanani 会进行两种操作,描述如下:

1. \(+\) \(l\) \(x\) 表示往第 \(l\) 层香槟塔倒 \(x\) 单位的酒。倒酒的规则是,如果某一层香槟塔的容量已满,酒会溢出到下面第一个容量更大的香槟塔中。如果下面已经没有容量更大的香槟塔了,那么酒会被浪费掉

2. \(?\) \(l\) 表示查询第 \(l\) 层香槟塔当前有多少单位的酒

如图为样例2的样例解释

题目解析:要是当初能过这题和疲劳值,说不定积分就够了……这个题目是不断地倒酒,那么我们考虑使用单调栈,先找位置i满了去哪里

具体实现:

  stack<int> stk;
  for (int i = n - 1; i >= 0; i --) {
    while (stk.size() && a[stk.top()] <= a[i]) stk.pop();
    next[i] =  stk.empty() ? -1 : stk.top();
    stk.push(i);
  }

我们从 下往上(即从 (n \to 1))扫描。

栈里维护的是一个 严格递减的容量序列(从栈底到栈顶)。

为什么?

当我们在位置 (i) 时:

  • 如果栈顶元素 (a[st.top()] \le a[i]),
    那么说明它永远不可能成为 (i) 的“下一个更大元素”,
    因为 (a[i]) 已经更大了,而且位置更靠上,
    所以把它弹掉。

  • 当栈顶元素 (a[st.top()] > a[i]),
    那么栈顶就是离 (i) 最近的、比它大的位置,
    也就是 (nxt[i])。

最后把 (i) 自己入栈。

这样保证了:栈从底到顶是 严格递减

也就找到了每个位置往下第一个容量更大的香槟塔

剩下的,边看代码边解释吧。

注意:这是第一版代码,这个代码会TLE,还需要进行优化

#include <bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;

using u128 = unsigned __int128;
using i128 = __int128;

using namespace std;

#define F(i, a, b) for (int i = (a); i <= (b); i++)
#define Fd(i, a, b) for (int i = (a); i >= (b); i--)

#ifndef DEBUG
struct __X {
  __X& operator<<(const auto& str) { return *this; }
  void sp(const std::string& str = "") {}
} dout;
#define debug(x);
#endif

constexpr int mod = 998244353;
constexpr int MOD = 1e9 + 7;
constexpr int inf = 1e9;
constexpr int N = 2e5 + 10;

signed main() {
  std::cin.tie(nullptr)->std::ios::sync_with_stdio(false);
  int n, q;
  cin >> n >> q;
  // a:香槟塔的容量
  vector<int> a(n);
  for (auto &x : a) cin >> x;

  // next:下一个是谁
  // re:剩余多少容量
  vector<int> next(n), re = a;
  stack<int> stk;
  for (int i = n - 1; i >= 0; i --) {
    while (stk.size() && a[stk.top()] <= a[i]) stk.pop();
    next[i] =  stk.empty() ? -1 : stk.top();
    stk.push(i);
  }

  // 封装成函数,方便反复调用
  auto update = [&](this auto &&self, int l, int &x) {
    // 如果倒的不多,可以装下,直接装
    // 反之,我们要往下进行查找“下一个可以装酒的层”
    if (x <= re[l]) {
      re[l] -= x;
      return;
    } else {
      x -= re[l];
      re[l] = 0;
      if (next[l] == -1) {
        return;
      } else {
        self(next[l], x);
      }
    }
  };

  // 注意题目是1-indexed,我是0-indexed
  while (q --) {
    char op;
    int l, x;
    cin >> op;
    if (op == '+') {
      cin >> l >> x;
      update(-- l, x);
    } else {
      cin >> l;
      l --;
      cout << a[l] - re[l] << '\n';
    }
    debug(re);
  }
}


/* By Tangzy
⠄⠄⠄⠄⢠⣿⣿⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⣿⣿⣯⢻⣿⣿⣿⣿⣆⠄⠄⠄
⠄⠄⣼⢀⣿⣿⣿⣿⣏⡏⠄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢻⣿⣿⣿⣿⡆⠄⠄
⠄⠄⡟⣼⣿⣿⣿⣿⣿⠄⠄⠄⠈⠻⣿⣿⣿⣿⣿⣿⣿⣇⢻⣿⣿⣿⣿⠄⠄
⠄⢰⠃⣿⣿⠿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠙⠿⣿⣿⣿⣿⣿⠄⢿⣿⣿⣿⡄⠄
⠄⢸⢠⣿⣿⣧⡙⣿⣿⡆⠄⠄⠄⠄⠄⠄⠄⠈⠛⢿⣿⣿⡇⠸⣿⡿⣸⡇⠄
⠄⠈⡆⣿⣿⣿⣿⣦⡙⠳⠄⠄⠄⠄⠄⠄⢀⣠⣤⣀⣈⠙⠃⠄⠿⢇⣿⡇⠄
⠄⠄⡇⢿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠄⣠⣶⣿⣿⣿⣿⣿⣿⣷⣆⡀⣼⣿⡇⠄
⠄⠄⢹⡘⣿⣿⣿⢿⣷⡀⠄⢀⣴⣾⣟⠉⠉⠉⠉⣽⣿⣿⣿⣿⠇⢹⣿⠃⠄
⠄⠄⠄⢷⡘⢿⣿⣎⢻⣷⠰⣿⣿⣿⣿⣦⣀⣀⣴⣿⣿⣿⠟⢫⡾⢸⡟⠄.
⠄⠄⠄⠄⠻⣦⡙⠿⣧⠙⢷⠙⠻⠿⢿⡿⠿⠿⠛⠋⠉⠄⠂⠘⠁⠞⠄⠄⠄
⠄⠄⠄⠄⠄⠈⠙⠑⣠⣤⣴⡖⠄⠿⣋⣉⣉⡁⠄⢾⣦⠄⠄⠄⠄⠄⠄⠄⠄
*/

交上去,果不其然的TLE。为什么呢?假设我每次都往第1层倒酒,然后第1层到第n - 1层都满了,那么我就会调用n次这个函数。整体的时间复杂度是O(nq),而n,q的最大值是300000(3e5),乘一下变成9e10,这是无法接受的。

所以,我们不要考虑反复调用函数,需要去动态维护下一个是谁

AC代码

#include <bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;

using u128 = unsigned __int128;
using i128 = __int128;

using namespace std;

#define F(i, a, b) for (int i = (a); i <= (b); i++)
#define Fd(i, a, b) for (int i = (a); i >= (b); i--)

#ifndef DEBUG
struct __X {
  __X& operator<<(const auto& str) { return *this; }
  void sp(const std::string& str = "") {}
} dout;
#define debug(x);
#endif

constexpr int mod = 998244353;
constexpr int MOD = 1e9 + 7;
constexpr int inf = 1e9;
constexpr int N = 2e5 + 10;

signed main() {
  std::cin.tie(nullptr)->std::ios::sync_with_stdio(false);
  int n, q;
  cin >> n >> q;
  vector<int> a(n);
  for (auto &x : a) cin >> x;

  vector<int> next(n), re = a;
  stack<int> stk;
  for (int i = n - 1; i >= 0; i --) {
    while (stk.size() && a[stk.top()] <= a[i]) stk.pop();
    next[i] =  stk.empty() ? -1 : stk.top();
    stk.push(i);
  }

  auto update = [&](int l) {
    // path:把哪些地方填满了
    // cur:现在在哪里
    vector<int> path;
    int cur = l;
    // 不断去跳cur指针,指向下一个合法的位置
    while (cur != -1 && re[cur] == 0) {
      path.push_back(cur);
      cur = next[cur];
    }
    // 把所有已经填满的地方记忆化,让他们下次好找
    for (int node : path) next[node] = cur;
    return cur;
  };

  while (q --) {
    char op;
    int l, x;
    cin >> op;
    if (op == '+') {
      cin >> l >> x;
      l --;
      int cur = l;
      while (x > 0 && cur != -1) {
        if (re[cur] >= x) {
          re[cur] -= x;
          x = 0;
        } else {
          x -= re[cur];
          re[cur] = 0;
          cur = update(cur);
        }
      }
    } else {
      cin >> l;
      l --;
      cout << a[l] - re[l] << '\n';
    }
    debug(re);
  }
}


/* By Tangzy
⠄⠄⠄⠄⢠⣿⣿⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⣿⣿⣯⢻⣿⣿⣿⣿⣆⠄⠄⠄
⠄⠄⣼⢀⣿⣿⣿⣿⣏⡏⠄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢻⣿⣿⣿⣿⡆⠄⠄
⠄⠄⡟⣼⣿⣿⣿⣿⣿⠄⠄⠄⠈⠻⣿⣿⣿⣿⣿⣿⣿⣇⢻⣿⣿⣿⣿⠄⠄
⠄⢰⠃⣿⣿⠿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠙⠿⣿⣿⣿⣿⣿⠄⢿⣿⣿⣿⡄⠄
⠄⢸⢠⣿⣿⣧⡙⣿⣿⡆⠄⠄⠄⠄⠄⠄⠄⠈⠛⢿⣿⣿⡇⠸⣿⡿⣸⡇⠄
⠄⠈⡆⣿⣿⣿⣿⣦⡙⠳⠄⠄⠄⠄⠄⠄⢀⣠⣤⣀⣈⠙⠃⠄⠿⢇⣿⡇⠄
⠄⠄⡇⢿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠄⣠⣶⣿⣿⣿⣿⣿⣿⣷⣆⡀⣼⣿⡇⠄
⠄⠄⢹⡘⣿⣿⣿⢿⣷⡀⠄⢀⣴⣾⣟⠉⠉⠉⠉⣽⣿⣿⣿⣿⠇⢹⣿⠃⠄
⠄⠄⠄⢷⡘⢿⣿⣎⢻⣷⠰⣿⣿⣿⣿⣦⣀⣀⣴⣿⣿⣿⠟⢫⡾⢸⡟⠄.
⠄⠄⠄⠄⠻⣦⡙⠿⣧⠙⢷⠙⠻⠿⢿⡿⠿⠿⠛⠋⠉⠄⠂⠘⠁⠞⠄⠄⠄
⠄⠄⠄⠄⠄⠈⠙⠑⣠⣤⣴⡖⠄⠿⣋⣉⣉⡁⠄⢾⣦⠄⠄⠄⠄⠄⠄⠄⠄
*/

这道题是我的遗憾。希望明年的今天,我可以把这种题当签到做吧。

posted @ 2025-08-31 22:02  Tangzy0121  阅读(444)  评论(0)    收藏  举报