1. 程式人生 > 實用技巧 >CodeForces 258E - Little Elephant and Tree

CodeForces 258E - Little Elephant and Tree

這題雖然水但是有好多方法,是個不可多得的好題(

洛谷題目頁面傳送門 & CF 題目頁面傳送門

題意見洛谷裡的翻譯。

首先,容易想到的是,對於每個節點維護與它有交集的節點集合。顯然,設 \(subtree(x)\) 表示子樹 \(x\) 內的節點集合,那麼第 \(i\) 次操作就是將 \(subtree(a_i)\cup subtree(b_i)\) 內所有節點的維護的集合並上 \(subtree(a_i)\cup subtree(b_i)\)。注意到,看 DFS 序的話,是兩個區間,由於是靜態的,可以差分,對於每個節點維護一個要並上的區間序列和一個要消除影響的序列。實際上這一步操作用樹上差分更好理解?就是子樹修改的話在根打個標記,然後每個節點要算上祖先貢獻和,一路 DFS 下來該回溯回溯。

接下來的難點在於:如何維護一個數據結構,支援:

  1. 給當前集合並上一個區間;
  2. 消除以前某次操作 \(1\) 的影響;
  3. 查詢當前不在集合內的數的數量(實際上應該是當前集合的大小,為了方便就維護這個,減一下就可以了)。

方法一

大家好我是 yxh,我用我最喜歡的暴力資料結構——分塊 A 了這道題(其實並沒有,她寫掛了)

很簡單,每個塊內維護一個基於被插入次數的桶和整體偏移量即可。塊長取 \(\sqrt n\) 的時候時空複雜度皆為 \(\mathrm O(n\sqrt n)\)。注意,空間卡得很緊,桶要用 short 開。

程式碼(現場,下面 \(3\) 種程式碼都是這個版本魔改的):

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m; 
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
	mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		if(y==fa)continue;
		dfs(y,x);
		mxdfn[x]=mxdfn[y];
	}
}
int d[N+2];
struct dvdblk{
	int sz,sz1;
	int a[N+1];
	struct block{int l,r,added;short cnt[2*M+1];}blk[DB_SZ];
	#define l(p) blk[p].l
	#define r(p) blk[p].r
	#define cnt(p) blk[p].cnt
	#define added(p) blk[p].added
	void bld(int p,int l,int r){
		l(p)=l;r(p)=r;
		memset(cnt(p),0,sizeof(cnt(p)));
		cnt(p)[m]=r-l+1;
		added(p)=0;
	}
	void init(){
		sz1=sqrt(n);
		sz=(n+sz1-1)/sz1;
		for(int i=1;i<=sz;i++)bld(i,(i-1)*sz1+1,min(i*sz1,n));
	}
	void add(int l,int r,int v){
		int pl=(l+sz1-1)/sz1,pr=(r+sz1-1)/sz1;
		if(pl==pr){
			for(int i=l;i<=r;i++)cnt(pl)[a[i]+m]--,cnt(pl)[(a[i]+=v)+m]++;
			return;
		}
		add(l,r(pl),v),add(l(pr),r,v);
		for(int i=pl+1;i<pr;i++)added(i)+=v;
	}
	int zero(){
		int res=0;
		for(int i=1;i<=sz;i++)res+=cnt(i)[m-added(i)];
		return res;
	}
}db;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
	freopen("b.in","r",stdin);freopen("b.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		nei[x].pb(y);nei[y].pb(x);
	}
	dfs();
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
		if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
		}
		else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
		else{
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
	}
	for(int i=1;i<=n;i++)d[i]+=d[i-1];
	db.init();
	for(int i=1;i<=n;i++){
		for(int j=0;j<add[i].size();j++){
			int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
			db.add(l,r,v);
		}
//		cout<<db.zero()<<"!\n";
		ans[mng[i]]=n-db.zero()-!!d[i];
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," \n"[i==n]);
	return 0;
}

方法二

不難發現剛剛那個分塊演算法其實是遠強於這題的需要的。追求更低的複雜度,嘗試用線段樹解決。一樣維護每個數的插入次數,然後每個節點內維護當前區間內 \(0\) 的數量。

注意到這題相比於分塊這個強演算法的特殊性質:永遠查詢 \(0\) 的數量,而 \(0\) 是值域裡的最小值。

注意到有區間修改,嘗試懶標記。然後發現,要加的話,顯然根據性質,當前區間就沒有 \(0\) 了;但如果要減的話,哦吼,你猜不到答案了。

於是嘗試標記永久化。哦吼,可以了!修改時在樹中經過的節點,用上傳更新它們。考慮如何作用當前節點的標記的貢獻:若為 \(0\),相當於沒有貢獻,直接上傳兩個兒子;若 \(>0\),則 \(0\)

的數量為 \(0\);由於每次減法是對之前加法的消除,所以標記恆非負。

時間 \(\mathrm O(m\log n)\),空間 \(\mathrm O(n)\)

程式碼:

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m; 
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
	mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		if(y==fa)continue;
		dfs(y,x);
		mxdfn[x]=mxdfn[y];
	}
}
int d[N+2];
struct segtree{
	struct node{int l,r,cnt,lz;}nd[N<<2];
	#define l(p) nd[p].l
	#define r(p) nd[p].r
	#define cnt(p) nd[p].cnt
	#define lz(p) nd[p].lz
	void bld(int l=1,int r=n,int p=1){
		l(p)=l;r(p)=r;lz(p)=0;cnt(p)=r-l+1;
		if(l==r)return;
		int mid=l+r>>1;
		bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
	}
	void init(){bld();}
	void sprup(int p){
		if(l(p)==r(p))cnt(p)=!lz(p);
		else cnt(p)=lz(p)?0:cnt(p<<1)+cnt(p<<1|1);
	}
	void add(int l,int r,int v,int p=1){
		if(l<=l(p)&&r>=r(p))return lz(p)+=v,sprup(p);
		int mid=l(p)+r(p)>>1;
		if(l<=mid)add(l,r,v,p<<1);
		if(r>mid)add(l,r,v,p<<1|1);
		sprup(p);
	}
	int zero(){return cnt(1);}
}segt;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
	freopen("b.in","r",stdin);freopen("b.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		nei[x].pb(y);nei[y].pb(x);
	}
	dfs();
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
		if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
		}
		else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
		else{
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
	}
	for(int i=1;i<=n;i++)d[i]+=d[i-1];
	segt.init();
	for(int i=1;i<=n;i++){
		for(int j=0;j<add[i].size();j++){
			int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
			segt.add(l,r,v);
		}
		ans[mng[i]]=n-segt.zero()-!!d[i];
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," \n"[i==n]);
	return 0;
}

方法三

這是 rng 的一個很巧妙的方法。

繼續使用上面那個性質。於是我們只要知道,最小值是多少,最小值有幾個即可,判斷最小值是否為 \(0\) 即可。

於是維護這兩個資訊,然後你會發現這是可以輕鬆懶標記和上傳的。上傳的時候,如果兩邊最小值不同就取小的,否則合併。

複雜度跟上面一個方法一樣。

程式碼:

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m; 
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
	mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		if(y==fa)continue;
		dfs(y,x);
		mxdfn[x]=mxdfn[y];
	}
}
int d[N+2];
struct segtree{
	struct node{int l,r,mn,cnt,lz;}nd[N<<2];
	#define l(p) nd[p].l
	#define r(p) nd[p].r
	#define mn(p) nd[p].mn
	#define cnt(p) nd[p].cnt
	#define lz(p) nd[p].lz
	void bld(int l=1,int r=n,int p=1){
		l(p)=l;r(p)=r;mn(p)=lz(p)=0;cnt(p)=r-l+1;
		if(l==r)return;
		int mid=l+r>>1;
		bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
	}
	void init(){bld();}
	void sprup(int p){
		if(mn(p<<1)==mn(p<<1|1))mn(p)=mn(p<<1),cnt(p)=cnt(p<<1)+cnt(p<<1|1);
		else{
			if(mn(p<<1)<mn(p<<1|1))mn(p)=mn(p<<1),cnt(p)=cnt(p<<1);
			else mn(p)=mn(p<<1|1),cnt(p)=cnt(p<<1|1);
		}
	}
	void tag(int p,int v){
		mn(p)+=v;
		lz(p)+=v;
	}
	void sprdwn(int p){
		tag(p<<1,lz(p));tag(p<<1|1,lz(p));
		lz(p)=0;
	}
	void add(int l,int r,int v,int p=1){
		if(l<=l(p)&&r>=r(p))return tag(p,v);
		sprdwn(p);
		int mid=l(p)+r(p)>>1;
		if(l<=mid)add(l,r,v,p<<1);
		if(r>mid)add(l,r,v,p<<1|1);
		sprup(p);
	}
	int zero(){return mn(1)==0?cnt(1):0;}
}segt;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
	freopen("b.in","r",stdin);freopen("b.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		nei[x].pb(y);nei[y].pb(x);
	}
	dfs();
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
		if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
		}
		else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
		else{
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
	}
	for(int i=1;i<=n;i++)d[i]+=d[i-1];
	segt.init();
	for(int i=1;i<=n;i++){
		for(int j=0;j<add[i].size();j++){
			int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
			segt.add(l,r,v);
		}
		ans[mng[i]]=n-segt.zero()-!!d[i];
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," \n"[i==n]);
	return 0;
}

方法四

不難發現這題還有另一個關於操作時效性的性質:由於樹可以表示成括號序列,所以任意一次減法(即對加法的消除影響)都是撤銷操作。

ymx 傻乎乎的維護了一個主席樹,區間賦 \(1\)、回到歷史版本、區間求和。其實根本不需要(只是我不會寫的說),用我自己發明的「可撤銷線段樹」即可。這個東西思想很簡單,跟可撤銷並查集一樣,每一次操作在棧中壓入一個修改序列,把所有有修改的節點的原樣給記錄下來,撤銷的時候就還原就可以了。

不難發現,這樣空間跟主席樹一樣,是 \(\mathrm O(m\log n)\) 的(捂臉

程式碼:

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m; 
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
	mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		if(y==fa)continue;
		dfs(y,x);
		mxdfn[x]=mxdfn[y];
	}
}
int d[N+2];
struct segtree{
	struct node{int l,r,sum;bool lz;}nd[N<<2];
	#define l(p) nd[p].l
	#define r(p) nd[p].r
	#define sum(p) nd[p].sum
	#define lz(p) nd[p].lz
	stack<stack<pair<int,node> > > stk;
	void bld(int l=1,int r=n,int p=1){
		l(p)=l;r(p)=r;sum(p)=r-l+1;lz(p)=0;
		if(l==r)return;
		int mid=l+r>>1;
		bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
	}
	void init(){bld();}
	void sprup(int p){
		stk.top().push(mp(p,nd[p]));
		sum(p)=sum(p<<1)+sum(p<<1|1);
	}
	void tag(int p){
		stk.top().push(mp(p,nd[p]));
		sum(p)=0;lz(p)=true;
	}
	void sprdwn(int p){
		stk.top().push(mp(p,nd[p]));
		if(lz(p))tag(p<<1),tag(p<<1|1),lz(p)=false;
	}
	void chg(int l,int r,int p=1){
		if(p==1)stk.push(stack<pair<int,node> >());
		if(l<=l(p)&&r>=r(p))return tag(p);
		sprdwn(p);
		int mid=l(p)+r(p)>>1;
		if(l<=mid)chg(l,r,p<<1);
		if(r>mid)chg(l,r,p<<1|1);
		sprup(p);
	}
	int zero(){return sum(1);}
	void cancel(){
		stack<pair<int,node> > s=stk.top();
		stk.pop();
		while(s.size())nd[s.top().X]=s.top().Y,s.pop();
	}
}segt;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
	freopen("b.in","r",stdin);freopen("b.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		nei[x].pb(y);nei[y].pb(x);
	}
	dfs();
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
		if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
		}
		else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
		else{
			add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
			add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
			add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
			add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
		}
	}
	for(int i=1;i<=n;i++)d[i]+=d[i-1];
	segt.init();
	for(int i=1;i<=n;i++){
		for(int j=0;j<add[i].size();j++){
			int v=add[i][j].Y;
			if(v==-1)segt.cancel();
		}
		for(int j=0;j<add[i].size();j++){
			int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
			if(v==1)segt.chg(l,r);
		}
		ans[mng[i]]=n-segt.zero()-!!d[i];
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," \n"[i==n]);
	return 0;
}

對比

中間那次 WA 的不管,從下到上分別是第 \(1\sim4\) 種方法。

可見,分塊複雜度大,劣是正常的;中間兩種方法比較好也是理所當然的;而最後一種「我發明」的資料結構,雖然複雜度要優於分塊,但是時空卻被其他三種方法全方位吊打,說明我是 sb!!!