• 洛谷 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;
    }
    
  • 相关阅读:
    linux系统调用是通过软中断实现的吗
    Linux系统调用怎么和内核或底层驱动交互的
    strace命令
    linux 用户态和内核态以及进程上下文、中断上下文 内核空间用户空间理解
    C语言string.h常用函数总结
    shall的过去式和should怎么区分
    P(Y|X) 和 P(X,Y)
    Sourceinsight最佳配色方案及颜色字体调整方法
    float 为什么可以表示很大的整数
    协方差矩阵
  • 原文地址:https://www.cnblogs.com/ycx-akioi/p/Luogu-P3960.html
Copyright © 2020-2023  润新知