洛谷 P3960

洛谷题目页面传送门

题意见洛谷。

方法(1):平衡树

不难发现,行与行之间的操作是独立的,而都与最后一列有关。很自然地想到维护每一行的前(m-1)个元素和最后一列。

不难发现,一次((x,y))离队,可以分成两种情况:

  1. (y=m)。可以看作将最后一列的第(x)个移到最后;
  2. (y eq m)。可以看作:
    1. 将第(x)行第(y)个移到最后一列最后;
    2. 将最后一列第(x)个移到第(x)行最后。

这些维护序列的删除、插入、查询第(k)个,一看就是序列之王平衡树的操作,这里使用fhq-Treap。

至于,如果暴力维护平衡树的话,建树就要MLE,是(mathrm O(nm))的。考虑用fbb的OJ那题的trick,任意时刻,所有没有被拎出来的naive元素组成的极大区间(区间内编号连续),我们把它们缩成一个点。

代码里操作能合并的合并,修改操作也可以返回原值减少操作量,来减小常数,毕竟这不是正解/cy

由于要删除节点,我开了垃圾回收,其实根本不用,就当练习一下(

一开始觉得挺难,现在看来是个挺板的题啊。。时间复杂度(mathrm O(qlog n))(假设(n,m)同阶)。

这里讲一个我代码里犯的稀有的错误:这是我第一次用vector存节点写平衡树,其中建树的时候我是这样写的:

if(l<mid)lson(p)=bld(l,mid-1);
if(r>mid)rson(p)=bld(mid+1,r);

以第一句为例,这里lson(p)bld(l,mid-1)的执行顺序直觉是先后者后前者,但其实不是,究竟是先前者后后者还是UB我也说不清,详见这篇帖子。问题来了,bld函数会调用nwnd函数新建节点并调用vectorpush_back函数,这会使vector重新分配内存,导致lson(p)的地址变了,进而导致值赋不进去。我调这个代码的那天晚上还以为闹鬼了,差点把我整哭了(捂脸

所以应该找个中间变量存下来:

int res;
if(l<mid)res=bld(l,mid-1),lson(p)=res;
if(r>mid)res=bld(mid+1,r),rson(p)=res;

代码(开洛谷自带O2才能过哦,毒瘤):

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
typedef long long ll;
mt19937 rng(20060617);
const int N=300000;
int n,m,qu;
struct fhq_treap{//平衡树 
	int root;
	struct node{unsigned key;int lson,rson,sz,real_sz;ll l,r;};
	#define key(p) nd[p].key
	#define lson(p) nd[p].lson
	#define rson(p) nd[p].rson
	#define sz(p) nd[p].sz
	#define real_sz(p) nd[p].real_sz
	#define l(p) nd[p].l
	#define r(p) nd[p].r
	vector<node> nd;//vector存节点 
	stack<int> bin;//垃圾桶 
	int nwnd(ll l,ll r){//新建节点 
		if(l>r)return 0;
		if(bin.size()){//用垃圾 
			int p=bin.top();
			bin.pop();
			return nd[p]=node({rng(),0,0,1,int(r-l+1),l,r}),p;
		}
		return nd.pb(node({rng(),0,0,1,int(r-l+1),l,r})),nd.size()-1;
	}
	void sprup(int p){sz(p)=sz(lson(p))+1+sz(rson(p));real_sz(p)=real_sz(lson(p))+r(p)-l(p)+1+real_sz(rson(p));}
	int bld(int l=1,int r=n){//建树 
		int mid=l+r>>1,p=nwnd(1ll*mid*m,1ll*mid*m);
		int res;
		if(l<mid)res=bld(l,mid-1),lson(p)=res;
		if(r>mid)res=bld(mid+1,r),rson(p)=res;//讷,错误就在这里 
		sprup(p);
		return sprup(p),p;
	}
	void init(ll l=0,ll r=0){
		nd.pb(node({0,0,0,0,0,0,0}));
		if(l)root=nwnd(l,r);
		else root=bld();
	}
	pair<int,int> split(int x,int p=-1){~p||(p=root);
		if(!x)return mp(0,p);
		pair<int,int> sp;
		if(x<=sz(lson(p)))return sp=split(x,lson(p)),lson(p)=sp.Y,sprup(p),mp(sp.X,p);
		return sp=split(x-1-sz(lson(p)),rson(p)),rson(p)=sp.X,sprup(p),mp(p,sp.Y);
	}
	int mrg(int p,int q){
		if(!p||!q)return p|q;
		if(key(p)<key(q))return rson(p)=mrg(rson(p),q),sprup(p),p;
		return lson(q)=mrg(p,lson(q)),sprup(q),q;
	}
	pair<int,int> rk(int x,int p=-1){~p||(p=root);//在树中的排名(算naive区间) 
		if(x<=real_sz(lson(p)))return rk(x,lson(p));
		if(x<=real_sz(lson(p))+r(p)-l(p)+1)return mp(sz(lson(p))+1,x-real_sz(lson(p)));
		pair<int,int> res=rk(x-(real_sz(lson(p))+r(p)-l(p)+1),rson(p));
		return mp(sz(lson(p))+1+res.X,res.Y);
	}
	void recyc(int p){bin.push(p);}//垃圾回收 
	ll del(int x){//删除点 
		pair<int,int> _rk=rk(x);
		pair<int,int> sp=split(_rk.X-1),sp0=split(1,sp.Y);
		node tmp=nd[sp0.X];
		recyc(sp0.X);//垃圾回收 
		int l=nwnd(tmp.l,tmp.l+_rk.Y-2),r=nwnd(tmp.l+_rk.Y,tmp.r);
		return root=mrg(sp.X,mrg(l,mrg(r,sp0.Y))),tmp.l+_rk.Y-1;
	}
	ll chg_mv_bk(int x,ll v=0){//修改并移到最后 
		pair<int,int> _rk=rk(x);
		pair<int,int> sp=split(_rk.X-1),sp0=split(1,sp.Y);
		ll res=l(sp0.X)+_rk.Y-1;
		int l=nwnd(l(sp0.X),l(sp0.X)+_rk.Y-2),r=nwnd(l(sp0.X)+_rk.Y,r(sp0.X));
		l(sp0.X)=r(sp0.X)=v?v:res,real_sz(sp0.X)=1;
		return root=mrg(sp.X,mrg(l,mrg(r,mrg(sp0.Y,sp0.X)))),res;
	}
	void pb(ll v){//在最后压入 
		root=mrg(root,nwnd(v,v));
	}
}trp_r[N+1],&trp_c=trp_r[0];
int main(){
	cin>>n>>m>>qu;
	for(int i=1;i<=n;i++)trp_r[i].init(1ll*(i-1)*m+1,1ll*i*m-1);
	trp_c.init();//最后一列要普通建树,因为编号不连续/kk 
	while(qu--){
		int x,y;
		scanf("%d%d",&x,&y);
		if(y==m){//情况1 
			printf("%lld
",trp_c.chg_mv_bk(x));
		}
		else{//情况2 
			ll res=trp_r[x].del(y);
			printf("%lld
",res);
			trp_r[x].pb(trp_c.chg_mv_bk(x,res));
		}
	}
	return 0;
}

方法(2):动态开点线段树

回想当年不会平衡树的时候,想着咋用现有知识维护序列插入删除。一个想法是:给每个位置设一个值(0/1)(1)表示还健在,(0)表示被删了。删除操作就赋(0),查询第(k)个就二分出前缀和等于(k)的第一个位置,那么插入呢?就无能为力了。幸运的是,这题的插入操作只存在于末尾,于是我们在末尾直接新建节点即可。考虑到空间开不下,我们用动态开点线段树维护,查询的话线段树二分。然后大概也是跟平衡树一样缩点。

代码很难写,不想写了。常数应该小一点?

方法(3):BIT(正解)

考虑继续缩小常数。注意到,单点查询和在前缀和上二分(BIT倍增,这里由于BIT只能从左往右倍增最右值,而我们要查最左边的(geq k)的位置,只需要转化为最右边的(<k)的位置加一即可)刚好都是BIT能支持的,考虑使用BIT。

但是BIT不能动态开点,空间受不了怎么办呢?

考虑这样一个方法:将询问离线下来,依次处理每行的前(m-1)个元素(每行内部按时间戳排序),把查询的结果记下来,处理完之后撤销影响处理下一个,这样空间只需要开一个BIT,时间复杂度也是不变的。

由于没有按原顺序操作,我们并不能知道每个位置的具体编号。这样再从头按原顺序来一遍,此时每个行都不需要BIT了,把BIT留给最后一列实时操作,每行及最后一列开个vector记录插入末尾的编号,就可以实时查询任意合法位置的编号了。

代码(不开O2也每个点在(1mathrm s)内):

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
typedef long long ll;
int lowbit(int x){return x&-x;}
const int N=300000,QU=300000;
int n,m,qu;
struct query{int x,y,id;}qry[QU+1];
bool cmp(query x,query y){return x.id<y.id;}
vector<query> v[N+1];
struct bitree{//BIT 
	int sum[N+QU+1];
	void init(){memset(sum,0,sizeof(sum));}
	void add(int x,int v){//单点加 
		while(x<=max(n,m)+qu)sum[x]+=v,x+=lowbit(x);
	}
	int fd(int x){//BIT倍增 
		int res=0,now=0;
		for(int i=20;~i;i--)if(res+(1<<i)<=max(n,m)+qu&&now+sum[res+(1<<i)]<x)res+=1<<i,now+=sum[res];
		return res+1;
	}
}bit;
int fd[N+1];//记录查询结果 
vector<ll> bk_r[N+1],&bk_c=bk_r[0];
int main(){
	cin>>n>>m>>qu;
	for(int i=1;i<=qu;i++)scanf("%d%d",&qry[i].x,&qry[i].y),qry[i].id=i,qry[i].y<m&&(v[qry[i].x].pb(qry[i]),0);
	bit.init();//初始化 
	for(int i=1;i<m;i++)bit.add(i,1);
	for(int i=1;i<=n;i++){//离线操作 
		sort(v[i].begin(),v[i].end(),cmp);//按时间戳排序 
		for(int j=0;j<v[i].size();j++){
			int y=v[i][j].y,id=v[i][j].id;
			fd[id]=bit.fd(y);//查询并记录 
			bit.add(fd[id],-1);bit.add(m+j,1);//删除、插入 
		}
		for(int j=0;j<v[i].size();j++)bit.add(fd[v[i][j].id],1),bit.add(m+j,-1);//撤销影响 
	}
	bit.init();//重置 
	for(int i=1;i<=n;i++)bit.add(i,1);
	for(int i=1;i<=qu;i++){
		int x=qry[i].x,y=qry[i].y;
		if(y==m){
			int _fd=bit.fd(x);
			ll res=_fd<=n?1ll*_fd*m:bk_c[_fd-n-1];//从vector里查 
			printf("%lld
",res);
			bk_c.pb(res);//压入vector 
			bit.add(_fd,-1);bit.add(n+bk_c.size(),1);//删除、插入 
		}
		else{
			ll res=fd[i]<m?1ll*(x-1)*m+fd[i]:bk_r[x][fd[i]-m];//从vector里查 
			printf("%lld
",res);
			int _fd=bit.fd(x);
			bk_r[x].pb(_fd<=n?1ll*_fd*m:bk_c[_fd-n-1]);bk_c.pb(res);//压入vector 
			bit.add(_fd,-1);bit.add(n+bk_c.size(),1);//删除、插入  
		}
	}
	return 0;
}
原文地址:https://www.cnblogs.com/ycx-akioi/p/Luogu-P3960.html