代码随想录算法训练营Day10 栈与队列
栈与队列
栈与队列的题目我做的比较少,感觉主要的用法是单调栈和单调队列,以此来记录某些内容。至于如何运用,核心是抓住“先进先出”(队列)和“先进后出”(栈)的特性。
想起来终测第一天的时候卡死我,导致有一个单调栈的题目我都没写,后面发现还简单一点。。。这里也一并记录。
Leetcode 232. 用栈实现队列
题目链接:https://leetcode.cn/problems/implement-queue-using-stacks/description/
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x)将元素 x 推到队列的末尾int pop()从队列的开头移除并返回元素int peek()返回队列开头的元素boolean empty()如果队列为空,返回true;否则,返回false
说明:
-
你 只能 使用标准的栈操作 —— 也就是说只有
push to top、peek/pop from top、size和is 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)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x)将元素 x 压入栈顶。int pop()移除并返回栈顶元素。int top()返回栈顶元素。boolean empty()如果栈是空的,返回true;否则,返回false。
注意:
-
你只能使用队列的标准操作 —— 也就是
push to back、peek/pop from front、size和is 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,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
题目解析:这个题目开始利用栈的性质了。简单来说,栈的性质可以用来“匹配”。如果有某种特性可以用来“匹配”,那么就可以考虑栈。
具体而言,这道题目需要无脑塞左括号,然后遇到匹配的右括号就弹出,因为他已经合法了。这样的原理是,如果合法,那么我们后加入栈的元素一定是先合法,可以形象化描述“先脱单”。
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`,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
题目解析:这个题目和上个题目一样,只是“脱单条件”变成了“相同”。
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
⠄⠄⠄⠄⢠⣿⣿⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⣿⣿⣯⢻⣿⣿⣿⣿⣆⠄⠄⠄
⠄⠄⣼⢀⣿⣿⣿⣿⣏⡏⠄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢻⣿⣿⣿⣿⡆⠄⠄
⠄⠄⡟⣼⣿⣿⣿⣿⣿⠄⠄⠄⠈⠻⣿⣿⣿⣿⣿⣿⣿⣇⢻⣿⣿⣿⣿⠄⠄
⠄⢰⠃⣿⣿⠿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠙⠿⣿⣿⣿⣿⣿⠄⢿⣿⣿⣿⡄⠄
⠄⢸⢠⣿⣿⣧⡙⣿⣿⡆⠄⠄⠄⠄⠄⠄⠄⠈⠛⢿⣿⣿⡇⠸⣿⡿⣸⡇⠄
⠄⠈⡆⣿⣿⣿⣿⣦⡙⠳⠄⠄⠄⠄⠄⠄⢀⣠⣤⣀⣈⠙⠃⠄⠿⢇⣿⡇⠄
⠄⠄⡇⢿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠄⣠⣶⣿⣿⣿⣿⣿⣿⣷⣆⡀⣼⣿⡇⠄
⠄⠄⢹⡘⣿⣿⣿⢿⣷⡀⠄⢀⣴⣾⣟⠉⠉⠉⠉⣽⣿⣿⣿⣿⠇⢹⣿⠃⠄
⠄⠄⠄⢷⡘⢿⣿⣎⢻⣷⠰⣿⣿⣿⣿⣦⣀⣀⣴⣿⣿⣿⠟⢫⡾⢸⡟⠄.
⠄⠄⠄⠄⠻⣦⡙⠿⣧⠙⢷⠙⠻⠿⢿⡿⠿⠿⠛⠋⠉⠄⠂⠘⠁⠞⠄⠄⠄
⠄⠄⠄⠄⠄⠈⠙⠑⣠⣤⣴⡖⠄⠿⣋⣉⣉⡁⠄⢾⣦⠄⠄⠄⠄⠄⠄⠄⠄
*/
这道题是我的遗憾。希望明年的今天,我可以把这种题当签到做吧。

浙公网安备 33010602011771号