CodeForces 258E

这题虽然水但是有好多方法,是个不可多得的好题(

洛谷题目页面传送门 & 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(nsqrt 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()<<"!
";
		ans[mng[i]]=n-db.zero()-!!d[i];
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," 
"[i==n]);
	return 0;
}

方法二

不难发现刚刚那个分块算法其实是远强于这题的需要的。追求更低的复杂度,尝试用线段树解决。一样维护每个数的插入次数,然后每个节点内维护当前区间内 (0) 的数量。

注意到这题相比于分块这个强算法的特殊性质:永远查询 (0) 的数量,而 (0) 是值域里的最小值。

注意到有区间修改,尝试懒标记。然后发现,要加的话,显然根据性质,当前区间就没有 (0) 了;但如果要减的话,哦吼,你猜不到答案了。

于是尝试标记永久化。哦吼,可以了!修改时在树中经过的节点,用上传更新它们。考虑如何作用当前节点的标记的贡献:若为 (0),相当于没有贡献,直接上传两个儿子;若 (>0),则 (0) 的数量为 (0);由于每次减法是对之前加法的消除,所以标记恒非负。

时间 (mathrm O(mlog 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]," 
"[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]," 
"[i==n]);
	return 0;
}

方法四

不难发现这题还有另一个关于操作时效性的性质:由于树可以表示成括号序列,所以任意一次减法(即对加法的消除影响)都是撤销操作。

ymx 傻乎乎的维护了一个主席树,区间赋 (1)、回到历史版本、区间求和。其实根本不需要(只是我不会写的说),用我自己发明的「可撤销线段树」即可。这个东西思想很简单,跟可撤销并查集一样,每一次操作在栈中压入一个修改序列,把所有有修改的节点的原样给记录下来,撤销的时候就还原就可以了。

不难发现,这样空间跟主席树一样,是 (mathrm O(mlog 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]," 
"[i==n]);
	return 0;
}

对比

中间那次 WA 的不管,从下到上分别是第 (1sim4) 种方法。

可见,分块复杂度大,劣是正常的;中间两种方法比较好也是理所当然的;而最后一种「我发明」的数据结构,虽然复杂度要优于分块,但是时空却被其他三种方法全方位吊打,说明我是 sb!!!

珍爱生命,远离抄袭!
原文地址:https://www.cnblogs.com/ycx-akioi/p/CodeForces-258E.html