【题解】UR #23 T2 地铁规划

注意到除了自己出的题之外,这好像是我写的第一篇题解……
由于题目中的各种神奇限制,出题人的意思就很明显了:让你用单栈(插入,撤销最近一次插入)实现一个队列(插入,撤销最早一次插入)。


考虑一个经典问题:双栈实现队列。我们将队列里的元素以某个分界点分成两半,前一半倒着插入栈A,后一半正着插入栈B。
当要插入元素时,我们插入栈B;当要弹出元素时,如果栈A非空,我们从栈A弹出;否则现在的队列一定是全部被正着插到了栈B中,我们需要把它们挪到栈A中方便弹出。
暴力的把它们一个一个弹出并按弹出的顺序插入栈A,则恰好满足我们的需求:这些元素插入栈A的顺序是原插入顺序的倒序。且不难分析出每个元素入栈、出栈次数皆为常数。
这样,容易发现栈B起的是一个缓冲的作用:当栈A为空时,我们才会从栈B弹出元素到栈A中,就避免了栈的弹出顺序和队列恰好相反的问题。


现在只有单栈,我们考虑沿用这个做法:将栈中元素分为两类,一类称为A类,是很快就要弹出的,其从栈底到栈顶的顺序,恰好原始插入顺序相反;一类称为B类,先在栈中缓冲一下,在栈内的顺序与原始插入顺序相同;等A类都被弹出了再把自己全部弹出,倒过来插入,变成A类元素。
但是单栈的问题在于我们无法将两类元素完美的分开。我们不妨就让它们混在一起,可这就导致弹出时栈顶的元素很可能不是A类。
所以我们在需要弹出元素时采取如下的策略:如果栈顶是A类直接弹出;如果是B类则不断弹出,暂存到另一个地方,直到弹出了一个A类为止,再将刚刚弹出的B类按原顺序插入。
这个方法浪费就浪费在它每次可能弹出一堆B类元素又重新插入回去,却仅仅用来弹出一个A类元素。于是我们考虑:每次多弹出一些A类元素,在插入回去的时候先插入B类,再插入这些额外弹出的A类,这样这些A类元素就离栈顶更近了。那具体弹出多少才合适呢?
这题的精妙之处就在这里了:加上我们需要弹出的那个A类元素,我们一共弹出 \(\text{lowbit}(cnt)\) 个A类元素,其中 \(cnt\) 是当前栈中的A类元素个数。
我们来证明它的操作次数复杂度:我们考虑将栈中的A类元素分为若干个“组”,\(cnt\) 中的每个二进制位为每个组的大小,因此同时至多有 \(\log\) 个组。这样,之前的策略相当于将栈中最后一整个组的位置和最后连续的一段B类元素的位置互换了。
所以如果我们定义一个B类元素的势能是它前面的组的个数,则移动B类元素的操作次数很明显就是 \(\mathcal O(m\log n)\) 了。
那移动 \(A\) 类元素呢?设某个A类元素所在的组的大小为 \(2^k\),则将其势能定义成 \(k\)。由于每次移动A类元素后移动会弹出一个A类元素,且其所在的组的大小恰为 \(\text{lowbit}(cnt)=2^{\lambda}\),那么这个组在删除这个元素后,会分裂成若干个大小分别为 \(2^{\lambda-1},2^{\lambda-2}\ldots 1\) 的组,至此移动A类元素的复杂度也明显是 \(\mathcal O(m\log n)\) 的啦。

贴个代码:

#include "subway.h"
#include<cstdio>
#include<cassert>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<cstdlib>
using namespace std;

int read();
typedef long long ll;
#define fr(i,l,r) for(int i=(l);i<=(r);++i)
#define rf(i,l,r) for(int i=(l);i>=(r);--i)
#define fo(i,l,r) for(int i=(l);i<(r);++i)
#define foredge(i,u,v) for(int i=fir[u],v;v=to[i],i;i=nxt[i])

const int N=2e5+5;
int n,m;
int stk[N],top,cnt,typ[N];
void init(int _n,int _m,int _lim) {n=_n,m=_m;}
int solve(int l) {
	static int r=0;
	while(r<m&&check(r+1)) merge(stk[++top]=++r),typ[top]=1;
	if(!cnt) {
		fr(i,1,top) undo();
		reverse(stk+1,stk+top+1);
		fr(i,1,top) typ[i]=0,merge(stk[i]);
		cnt=top;
	} else {
		int c1=0,c0=cnt&-cnt;
		while(typ[top-c1]) undo(),++c1;
		fr(i,1,c0) undo();
		fr(i,1,c0) typ[top+1-i]=stk[top-c1+1-i];
		fr(i,1,c1) typ[top-c0+1-i]=stk[top+1-i];
		fr(i,top-c0-c1+1,top) merge(stk[i]=typ[i]);
		fr(i,1,c0) typ[top+1-i]=0;
		fr(i,1,c1) typ[top-c0+1-i]=1;
	}
	assert(typ[top]==0);
	assert(stk[top--]==l);
	return--cnt,undo(),r;
}

顺带一提,这个势能分析的方法和基数堆的非常相似。或许它还有更多的拓展应用?
这种拿栈模拟队列的方法很通用,允许双栈的情况下还能模拟双端队列。在某些只能撤销的题目中很有用(loj6515)。

posted @ 2022-04-03 18:56  秋叶冬雪  阅读(256)  评论(0)    收藏  举报