1. 程式人生 > 實用技巧 >HDOJ 6756 - Finding a MEX

HDOJ 6756 - Finding a MEX

前天的多校題。朝鮮就是毒瘤,簽到題都這麼難/kk

HDOJ題目頁面傳送門

給定一個無向圖\(G=(V,E),|V|=n,|E|=m\),每個點有點權\(a_i\),設\(S_i=\{a_j\mid (i,j)\in E\}\),支援\(2\)\(q\)次操作:

  1. \(\texttt1\ x\ y\):令\(a_x=y\)
  2. \(\texttt2\ x\):求\(\mathrm{mex}(S_x)\)

\(n,m\in\left[1,10^5\right],a_i\in\left[0,10^9\right]\)。本題多測,最多有\(10\)組資料。\(\mathrm{TL}=3\mathrm s\)

以下認為\(n,m\)同階,複雜度裡用\(n\)表示。

對於無向圖上的詢問題,一般有一個套路:根號分治(還是聽yjz講課才會的)。考慮設一個界限\(lim\),將度數\(\leq lim\)的都設為小點,其他都是大點。不難發現,小點度數上限是\(\mathrm O(lim)\),大點個數上限是\(\mathrm O\!\left(\dfrac n{lim}\right)\)(因為\(\sum\limits_{i=1}^n|\delta(i)|=2m\)),這是兩個很好的性質。

於是分成四個需要思考的問題:小點修改,小點查詢,大點修改,大點查詢。

首先有一條引理:\(\mathrm{mex}(S)=\mathrm O(|S|)\)

。證明很容易,當\(S=\{0,1,2,\cdots\}\)時會把\(\mathrm{mex}(S)\)卡最大。注意到對於小點查詢,小點\(x\)的度數不會太大,那麼\(\mathrm{mex}(S_x)\)也不會太大,是\(\mathrm O(lim)\)級別的,考慮不用對它維護任何東西,直接暴力查。至於咋暴力,在外面開一個全域性的\([0,n]\)上的bool陣列(點權超過\(n\)可以直接當作\(n\)看,正確性顯然),每次小點查詢將鄰居點權賦上true,然後從頭遍歷,查完撤銷影響。

那麼對應的,大點查詢的話我們就需要維護一些東西來實現快速查詢;而無論是小點修改還是大點修改,都可以修改所有鄰居大點(這樣的點數量是\(\mathrm O\!\left(\dfrac m{lim}\right)\)

級別的)所維護的資料結構。

看到在集合裡查東西,很容易想到平衡樹,這裡使用fhq-Treap。這樣,改點權的時候是平衡樹正常操作(單點插入+單點刪除),\(\mathrm O(\log n)\);查\(\mathrm{mex}\)的時候,可以用類似二分的思想,在當前節點時,如果左子樹是滿的(即開頭到內部到當前節點之間沒有一點空隙),就往右子樹走,否則往左子樹走,時間複雜度與樹高成正比,\(\mathrm O(\log n)\)

下面來整理一下各操作的複雜度:

  1. 小點修改:平衡樹插入+刪除,\(\mathrm O\!\left(\dfrac n{lim}\log n\right)\)
  2. 小點查詢:暴力,\(\mathrm O(lim)\)
  3. 大點修改:平衡樹插入+刪除,\(\mathrm O\!\left(\dfrac n{lim}\log n\right)\)
  4. 大點查詢:平衡樹查詢,\(\mathrm O(\log n)\)

時間複雜度就是\(\mathrm O\!\left(q\left(\dfrac n{lim}\log n+lim\right)\right)\)。根據均值不等式,當\(lim\)\(\sqrt{m\log n}\)的時候最優,為\(\mathrm O\!\left(n\sqrt{n\log n}\right)\)

現場覺得這個複雜度有點懸,常數又大,寫好之後交上去,果然T了!這比WA還要狠。後來事實證明不是複雜度/常數的問題,是寫掛了。之後TLE->RE->WA->AC,太不容易了……

程式碼(現場rush的,還有sjc在旁邊不停囉嗦,有點醜,見諒):

#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
void read(int &x){
	x=0;char c=getchar();
	while(!isdigit(c))c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
void prt(int x){
	if(x>9)prt(x/10);
	putchar(x%10^48);
}
mt19937 rng(20060617);
const int N=100000;
int n,m,qu;
int a[N+1];
vector<int> nei[N+1];
vector<int> nei_big[N+1];
struct fhq_treap{//平衡樹 
	int sz,root;
	struct node{unsigned key;int lson,rson,sz,v,mx;}nd[2*N+1];
	#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 v(p) nd[p].v
	#define mx(p) nd[p].mx
	void init(){
		sz=root=0;
		nd[0]=node({0,0,0,0,0,0});
	}
	void sprup(int p){sz(p)=sz(lson(p))+1+sz(rson(p));mx(p)=max(mx(lson(p)),max(v(p),mx(rson(p))));}
	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;
	}
	int lss(int v,int p=-1){~p||(p=root);
		if(!p)return 0;
		if(v(p)<v)return sz(lson(p))+1+lss(v,rson(p));
		return lss(v,lson(p));
	}
	int nwnd(int v){return nd[++sz]=node({rng(),0,0,1,v,v}),sz;}
	void insert(int v){
//		cout<<"insert "<<v<<"\n";
		pair<int,int> sp=split(lss(v));
		root=mrg(mrg(sp.X,nwnd(v)),sp.Y);
	}
	void del(int v){
//		cout<<"del "<<v<<"\n";
		pair<int,int> sp=split(lss(v)),sp0=split(1,sp.Y);
		root=mrg(sp.X,sp0.Y);
	}
	int mex(int now=0,int p=-1){~p||(p=root);
//		printf("mex %d %d\n",now,v(p));
		if(!p)return now;
		if(v(p)-now==sz(lson(p)))return mex(v(p)+1,rson(p));
		return mex(now,lson(p));
	}
}trp[100];
int hav[100][N+1];
int big_id[N+1];
bool tmp[N+1];
void mian(){
	read(n);read(m);
	for(int i=1;i<=n;i++)nei[i].clear();
	for(int i=1;i<=n;i++)read(a[i]),a[i]=min(n,a[i]);
	for(int i=1;i<=m;i++){
		int x,y;
		read(x);read(y);
		nei[x].pb(y);nei[y].pb(x);
	}
	int lim=sqrt(m*log2(n));
	for(int i=1;i<=n;i++){
		nei_big[i].clear();
		for(int j=0;j<nei[i].size();j++)
			if(nei[nei[i][j]].size()>lim)nei_big[i].pb(nei[i][j]);
	}
	int now=0;
	for(int i=1;i<=n;i++)if(nei[i].size()>lim){
		trp[big_id[i]=now++].init();
		for(int j=0;j<=n;j++)hav[big_id[i]][j]=0;
		for(int j=0;j<nei[i].size();j++){
			if(!hav[big_id[i]][a[nei[i][j]]])trp[big_id[i]].insert(a[nei[i][j]]);
			hav[big_id[i]][a[nei[i][j]]]++;
//			cout<<big_id[i]<<"!\n";
		}
	}
	read(qu);
	while(qu--){
		int tp,x,y;
		read(tp);read(x);
		if(tp==1){//修改 
			read(y);
			y=min(n,y);
			for(int i=0;i<nei_big[x].size();i++){
				int z=nei_big[x][i];
				hav[big_id[z]][a[x]]--;
//				cout<<big_id[z]<<"!\n";
				if(!hav[big_id[z]][a[x]])trp[big_id[z]].del(a[x]);
				if(!hav[big_id[z]][y])trp[big_id[z]].insert(y);
				hav[big_id[z]][y]++;
			}
			a[x]=y;
		}
		else{
			if(nei[x].size()<=lim){//小點查詢 
				for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=true;
				for(int i=0;;i++)if(!tmp[i]){prt(i);putchar('\n');break;}
				for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=false;
			}
			else prt(trp[big_id[x]].mex()),putchar('\n');//大點查詢 
		}
	}
}
int main(){
	int testnum;
	cin>>testnum;
	while(testnum--)mian();
	return 0;
}

後來看到dls的題解,發現竟然還有\(\mathrm O(n\sqrt n)\)的演算法………………

回顧上面整理的各操作的複雜度,發現一個現象,平衡樹查詢的次數只有\(\mathrm O(q)\),而平衡樹插入+刪除的次數有\(\mathrm O(q\dfrac n{lim})\),這兩個操作複雜度卻一樣。那我們是不是可以犧牲一下查詢的複雜度,來實現插入+刪除複雜度的更優呢?飢渴的人們想要把插入+刪除弄成\(\mathrm O(1)\),而能有操作複雜度為\(\mathrm O(1)\)的資料結構並不多,比較典型的就是萬能的分塊。

考慮基於值域分塊,每塊\(sz1\)個數,既然要\(\mathrm O(1)\),插入+刪除的時候只能直接改一下就得走人;而對於查詢,考慮找到第一個不滿的塊,再在裡面暴力找,這樣看來,我們得維護每一塊內有的元素的個數,為了實現這個的維護,還得維護一個bool陣列,這些都是可以\(\mathrm O(1)\)修改的,於是查詢複雜度\(\mathrm O\!\left(sz1+\dfrac n{sz1}\right)\)。為了最優,\(sz1\)\(\sqrt n\)。此時不難發現,總複雜度已經優化到\(\mathrm O(n\sqrt n)\)了……

又是根號分治又是分塊,這不是lxl的作風麼/jk

程式碼(在原來程式碼上魔改的):

#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
void read(int &x){
	x=0;char c=getchar();
	while(!isdigit(c))c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
void prt(int x){
	if(x>9)prt(x/10);
	putchar(x%10^48);
}
mt19937 rng(20060617);
const int N=100000,DB_SZ1=333,DB_SZ=DB_SZ1;
int n,m,qu;
int a[N+1];
vector<int> nei[N+1];
vector<int> nei_big[N+1];
struct dvdblk{//分塊 
	int sz,sz1;
	struct block{int l,r,sz;bool hav[DB_SZ1];}blk[DB_SZ];
	#define l(p) blk[p].l
	#define r(p) blk[p].r
	#define sz(p) blk[p].sz
	#define hav(p) blk[p].hav
	void bldblk(int p,int l,int r){
		l(p)=l;r(p)=r;sz(p)=0;memset(hav(p),0,sizeof(hav(p)));
	}
	void init(){
		sz1=sqrt(n);
		sz=(n+sz1)/sz1;
		for(int i=1;i<=sz;i++)bldblk(i,(i-1)*sz1,min(n,i*sz1-1));
	}
	void insert(int v){
		int p=(v+sz1)/sz1;
		hav(p)[v-l(p)]=true;
		sz(p)++;
	}
	void del(int v){
		int p=(v+sz1)/sz1;
		hav(p)[v-l(p)]=false;
		sz(p)--;
	}
	int mex(){
		for(int i=1;i<=sz1;i++)if(sz(i)<r(i)-l(i)+1){
			for(int j=l(i);;j++)if(!hav(i)[j-l(i)])return j;
		}
	}
}db[333];
int hav[333][N+1];
int big_id[N+1];
bool tmp[N+1];
void mian(){
	read(n);read(m);
	for(int i=1;i<=n;i++)nei[i].clear();
	for(int i=1;i<=n;i++)read(a[i]),a[i]=min(n,a[i]);
	for(int i=1;i<=m;i++){
		int x,y;
		read(x);read(y);
		nei[x].pb(y);nei[y].pb(x);
	}
	int lim=sqrt(m);
	for(int i=1;i<=n;i++){
		nei_big[i].clear();
		for(int j=0;j<nei[i].size();j++)
			if(nei[nei[i][j]].size()>lim)nei_big[i].pb(nei[i][j]);
	}
	int now=0;
	for(int i=1;i<=n;i++)if(nei[i].size()>lim){
		db[big_id[i]=now++].init();
		for(int j=0;j<=n;j++)hav[big_id[i]][j]=0;
		for(int j=0;j<nei[i].size();j++){
			if(!hav[big_id[i]][a[nei[i][j]]])db[big_id[i]].insert(a[nei[i][j]]);
			hav[big_id[i]][a[nei[i][j]]]++;
//			cout<<big_id[i]<<"!\n";
		}
	}
	read(qu);
	while(qu--){
		int tp,x,y;
		read(tp);read(x);
		if(tp==1){//修改 
			read(y);
			y=min(n,y);
			for(int i=0;i<nei_big[x].size();i++){
				int z=nei_big[x][i];
				hav[big_id[z]][a[x]]--;
//				cout<<big_id[z]<<"!\n";
				if(!hav[big_id[z]][a[x]])db[big_id[z]].del(a[x]);
				if(!hav[big_id[z]][y])db[big_id[z]].insert(y);
				hav[big_id[z]][y]++;
			}
			a[x]=y;
		}
		else{
			if(nei[x].size()<=lim){//小點查詢 
				for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=true;
				for(int i=0;;i++)if(!tmp[i]){prt(i);putchar('\n');break;}
				for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=false;
			}
			else prt(db[big_id[x]].mex()),putchar('\n');//大點查詢 
		}
	}
}
int main(){
	int testnum;
	cin>>testnum;
	while(testnum--)mian();
	return 0;
}