专栏文章
不删除双指针(栈模拟队列)
算法·理论参与者 6已保存评论 7
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 7 条
- 当前快照
- 2 份
- 快照标识符
- @mlia9upn
- 此快照首次捕获于
- 2026/02/12 01:08 上周
- 此快照最后确认于
- 2026/02/19 01:17 13 小时前
队列
先给出一个问题描述,我们有一个二元函数 满足
在这样的定义下,对于一个 ,就会存在一个最小的 使得 。显然 是单调不降的,问题是求出所有的 。
双指针维护两个指针 ,来求出 序列。算法流程是从小到大枚举 ,然后一直检查 是否为 ,如果是,就将 加一,最后一直加到 ,这个 就是 。注意,这里 是所有 共同使用的一直增加的变量,因此 的移动量是 的。
双指针一般是用来尝试解决关于区间的问题的, 和区间 的信息有关。每次 增加或者 增加,就对应将一个元素加入或删除。这就像一个队列,我们要维护一个队列,支持尾部加入,头部删除,查询队列中元素算出的 的值。我们进行 次这三种操作就能完成双指针。
双栈模拟队列
如果信息不支持删除,但是有结合律且可以合并(例子:群,包括线性基),我们可以尝试双栈模拟队列。我们需要两个栈 ,它们的两个栈底是相连的,就像这样:(下标越小越靠近栈顶)
每次加入一个元素的时候,往 的栈顶压入元素。每次删除一个元素的时候,如果 栈非空,就弹出 栈栈顶;否则将 中元素全部倒出来,倒序压入 栈,再弹出 栈栈顶。信息在这个过程中的维护是简单的。
由于每个元素只会从 栈进入 栈恰好一次,因此时间复杂度为 。
CPPtemplate <class T>
struct queue1 {
T a[410], b[410], suf;
int topa, topb;
void clear() {
topa = topb = 0;
}
void push(const T& x) {
if (topa) suf = suf + x, a[++topa] = x;
else suf = a[++topa] = x;
}
void pop() {
if (topb) return --topb, void();
assert(topa);
swap(topa, topb);
b[1] = a[topb];
for (int i = 2; i <= topb; i++) b[i] = a[topb - i + 1] + b[i - 1];
--topb;
}
T sum() {
if (!topa && !topb) return /*零*/;
if (!topa) return b[topb];
if (!topb) return suf;
return b[topb] + suf;
}
};
单栈模拟队列
如果信息不支持删除,但是支持加入和撤销最近一次操作,同时信息之间有交换律(可能还需要结合律)(例子:可撤销并查集),那么可以花费一个额外的 因子进行单栈模拟队列。思路是从双栈模拟队列出发,将两个栈合并成一个栈:
每次加入元素,就将其作为 栈元素压入栈;每次删除最先一次加入的元素(出队),如果 栈非空(有 元素),就弹出 栈栈顶,但是会有 元素压在 上面使其无法弹出,我们将 上的 元素连同 全部弹出,再将 元素按照原顺序放回去,就相当于是把 删了。如果 栈是空的(没有 元素),那就将所有 元素全部弹出再逆序放回去(为了改顺序让最先进队的元素靠近栈顶),然后再把 删了。
但是这样复杂度肯定是错的, 元素下面可能压着很多 元素,如果反复出队就炸了。我们需要一个势能分析控制复杂度。提出一个理论,设有 个 元素,我们尝试出队的时候一口气弹出 个 元素以及这些元素头上压着它们的 元素,再将 元素压回去,再将 个 元素压回去,最后一个要弹掉的 元素就不放回去了。没有 元素的时候还是要把所有 元素弹出改成 元素的顺序。
为什么这样就对了呢?我们首先需要一个归纳假设,从栈底到栈顶, 元素形成若干个连续段,每个连续段的长度都是 , 从栈底到栈顶单调减(可能有部分段是连在一起的,但是可以拆成这样)。这样我们弹出 个 元素时,就实际上弹出的是最后一段 的连续段和它头上的 连续段,然后将这两段交换位置,将 弹出, 这个连续段立即拆为 这么多个连续段,连续段长度的总和是 (这些连续段是紧密相连的)。再证明完“所有 元素弹出改成 元素的顺序”的情况后,可以发现这个归纳就自动成立了。
现在设计势能函数以证明复杂度。一个 元素的势能是它前面 连续段的个数,每次拿出 元素时这个势能就会减一,而势能始终 ,因此这部分复杂度正确。一个 元素的势能是它所在 连续段的 的值,每次拿出 元素的时候 会往下减,而势能始终 ,因此这部分复杂度正确。还有很重要的“所有 元素弹出改成 元素的顺序”的情况,这个也显然对复杂度不影响。
因此复杂度就是每个元素加入或撤销 次。
CPPstruct queue2 {
stack<int, vector<int>> stk;
int cnt = 0;
void (*add)(int);
void (*undo)();
queue2() = default;
queue2(decltype(add) _add, decltype(undo) _undo) : add(_add), undo(_undo) {}
void push(int x) {
stk.push(x), add(x);
}
void pop() {
vector<int> tmp[2];
int lb = cnt & -cnt;
while (!stk.empty() && (int)tmp[0].size() < (lb ? lb : 1)) {
int x = stk.top();
stk.pop(), undo();
tmp[x > 0].push_back(x);
}
if (!cnt) {
swap(tmp[0], tmp[1]);
reverse(tmp[0].begin(), tmp[0].end());
for (int &x : tmp[0]) x = -x;
cnt = (int)tmp[0].size();
}
reverse(tmp[1].begin(), tmp[1].end());
for (int x : tmp[1]) add(x), stk.push(x);
reverse(tmp[0].begin(), tmp[0].end());
tmp[0].pop_back(), --cnt;
for (int x : tmp[0]) add(-x), stk.push(x);
}
};
相关推荐
评论
共 7 条评论,欢迎与作者交流。
正在加载评论...