• 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!!!

    珍爱生命,远离抄袭!
  • 相关阅读:
    jsp-servlet(2)响应HTML文档-书籍管理系统
    jsp-servlet(1)环境搭建(Tomcat和myeclipse)和基本概念
    MySQL(2)数据库 表的查询操作
    MySQL(1) 基本操作(MySQL的启动,表的创建,查询表的结构和表的字段的修改)
    Java 构造器Constructor 继承
    数据结构:链表的操作
    C++指针易错点梳理
    C++操作符重载
    无重复字符的最长子串-LeetCode-第3题-C++
    计算几何模板存储
  • 原文地址:https://www.cnblogs.com/ycx-akioi/p/CodeForces-258E.html
Copyright © 2020-2023  润新知