1. 程式人生 > 實用技巧 >洛谷 P3960 - 列隊

洛谷 P3960 - 列隊

洛谷題目頁面傳送門

題意見洛谷。

方法\(1\):平衡樹

不難發現,行與行之間的操作是獨立的,而都與最後一列有關。很自然地想到維護每一行的前\(m-1\)個元素和最後一列。

不難發現,一次\((x,y)\)離隊,可以分成兩種情況:

  1. \(y=m\)。可以看作將最後一列的第\(x\)個移到最後;
  2. \(y\neq m\)。可以看作:
    1. 將第\(x\)行第\(y\)個移到最後一列最後;
    2. 將最後一列第\(x\)個移到第\(x\)行最後。

這些維護序列的刪除、插入、查詢第\(k\)個,一看就是序列之王平衡樹的操作,這裡使用fhq-Treap。

至於,如果暴力維護平衡樹的話,建樹就要MLE,是\(\mathrm O(nm)\)

的。考慮用fbb的OJ那題的trick,任意時刻,所有沒有被拎出來的naive元素組成的極大區間(區間內編號連續),我們把它們縮成一個點。

程式碼裡操作能合併的合併,修改操作也可以返回原值減少操作量,來減小常數,畢竟這不是正解/cy

由於要刪除節點,我開了垃圾回收,其實根本不用,就當練習一下(

一開始覺得挺難,現在看來是個挺板的題啊。。時間複雜度\(\mathrm O(q\log 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\n",trp_c.chg_mv_bk(x));
		}
		else{//情況2 
			ll res=trp_r[x].del(y);
			printf("%lld\n",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也每個點在\(1\mathrm 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\n",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\n",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;
}