[JZOJ4769]【GDOI2017模拟9.9】graph

题目

描述

在这里插入图片描述

题目大意很明确了,所以不说……


思考历程

一看见这题,咦,这就是传说中的动态图吗?
普通的动态图是维护连通性,这题是维护它是否是二分图,换言之就是维护它是否有奇环。
好像很复杂的样子。
想用LCT搞一搞,但是搞了很久终究搞不出来。
如果这道题全部都是加入就好了,但对于删除,好像要影响很多东西……
想了很久终将放弃。


正解

现在主要的正解大体分为两种:

第一种方法是使用线段树
对于每一条边,预处理除它们的加入和删除时间。
可以把它们存在的时间看作时间轴上的一段区间。
然后就有个很强大的做法:将这条边塞入线段树中,也就是代表这段区间的线段树上O(lg⁡m)O(\lg m)O(lgm)个节点上。
将所有的边加进去,然后顺序遍历。对于每个节点,进入它时,将挂在它上面的所有边加入某个数据结构中;从它回到父亲时,将这些边从那个数据结构中删除。
这个数据结构用来维护它是否是二分图,我们可以用可持久化并查集来实现。
在线段树上一直往下走的过程中,可以通过当前的加入操作来判断它是否出现了奇环。如果没有出现,继续做下去;如果出现了,那么这个节点代表的区间的答案都是NO,直接记下,下面的就不用做了,直接回去。
显然每条边在线段树上挂的节点有O(lg⁡m)O(\lg m)O(lgm)个,每次对可持久化并查集的操作为O(lg⁡n)O(\lg n)O(lgn)的时间,所以时间复杂度是O(mlg⁡mlg⁡n)O(m\lg m\lg n)O(mlgmlgn).。
可以通过。

那么这种做法的优点在哪里?
如果是按照原来的方式从左到右计算,那么很难处理出来。
会经常遇到这种情况:先加入AAA,再加入BBB,到后面AAA先出来,然后BBB再出来。AAA出来后会对BBB产生影响,所以比较难处理。
如果BBB先出来,AAA再出来,那就比较好处理了。BBBAAA后进一次又出一次,对于之前的AAA没有什么影响,所以继续处理比较方便。
总结一下,如何处理得方便呢?就是让操作变成先入后出的模式。
将其转化成先入后出的模式,线段树无疑是个非常好的选择。因为先入后出让我们想到了,从而想到树的遍历。将操作转化成树的形式,自然就要用到线段树了。

类似的方法还有分治
实际上分治和线段树的做法在本质上是一模一样的,只不过实现方式不同。
将分治的那棵树画出来,其实那就是一棵线段树。
每次处理的时候,先将当前边集中的所有边扫一遍,完全覆盖整个区间的就加入可持久化并查集中,然后在区间中取个中点,将左端点在中点左边的边加入新边集中,递归下去做,右边同理。
它只是将所有边一起处理罢了,时间复杂度是一样的。

第二种做法比较高级,是用LCT维护的。
还是要将每条边的删除时间处理出来。
然后在做的过程中,维护一个标记jjj表示当前到jjj的答案都为NO
还有维护一棵关于删除时间的最大生成树
加边的时候,如果两个端点之间不连通,就连接两点。
否则截取两点之间的路径,找出这个环中删除时间最早的边(包括这条新加进去的边)。
如果这个环是奇环,就用这个时间更新一下jjj
将删除时间最早的边删去,加入这条边(如果被删的是这条边就不用加了)。
删边的时候,如果这个边被删过了,那就不删,否则就将其删了。
一直这么做下去就好。
时间复杂度是O(mlg⁡(n+m))O(m\lg (n+m))O(mlg(n+m)),不要忘记乘上LCT自带的超大常数……

但是这么做的理由是什么?
感觉上是正确的,实际上我也是感性理解的。
那我就感性地解释一遍:
对于一个奇环,它被破坏的最早时间就是环中最早的删除时间。
有可能这条边在后面会产生别的影响,但是在它被删除之前,答案都是NO
就算它能产生什么影响,在它被删除之后,这些影响都没有意义。
所以在奇环中删掉删除时间最早的边是正确的。
那么为什么偶环也要删边呢?
首先删边不会影响这一刻的正确性,因为二分图删了一条边之后还是二分图。
然后,如果在后面会造成什么影响,就是加入某条边形成奇环,而这个奇环经过这条被删的边。但由于这条边是在一个偶环里面的,这条边被删了不要紧,因为如果它们能形成一个奇环,那走另一边一定也可以形成一个奇环,因为偶数减奇数等于奇数。

上面的这两种做法都是离线做法,至于在线做法,我就不知道了……
不过动态图是在线的,能不能类似地做这题……可惜我不会动态图啊……


代码

以下是分治做法:

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <map>
#define N 300010
int V,n;
struct edge{
	int u,v;
} e[N];
int m;
int beg[N],end[N];
int p[N],tmp[N];//边集数组。所谓新边集不会真的开一个,而是将一堆边集中在一起继续做
int fa[N],siz[N],col[N];//并查集相关,其中col表示它和父亲的关系:0表示相同,1反之
inline void get(int &x,int &c){//c为x和x的根的关系
	c=0;
	while (x!=fa[x])
		c^=col[x],x=fa[x];
}
int bz[N];//标记数组,表示在做某条边的时候加入了并查集上的那一条边(用边的儿子表示)
bool ans[N];
void dfs(int l,int r,int st,int en){//p[st..en]表示当前的边集数组
	int i,j,k;
	for (i=st;i<=en;++i){
		if (beg[p[i]]<=l && r<=end[p[i]]){
			int u=e[p[i]].u,v=e[p[i]].v,cu,cv;
			get(u,cu),get(v,cv);
			if (u!=v){
				if (siz[u]>siz[v])
					swap(u,v);
				fa[u]=v,siz[v]+=siz[u];
				col[u]=(cu==cv);//由于u和v必须不同,所以当cu和cv相同时,两个根不同;反之同理
				bz[p[i]]=u;//标记增加的边
			}
			else{
				bz[p[i]]=0;
				if (cu==cv)
					break;
			}
		}
	}
	if (i<=en){
		for (--i;i>=st;--i)//还原
			if (bz[p[i]]){
				int t=bz[p[i]];
				siz[fa[t]]-=siz[t];
				fa[t]=t;
			}
		for (i=l;i<=r;++i)
			ans[i]=0;
		return;
	}
	if (l==r)
		ans[l]=1;
	else{
		int mid=l+r>>1;
		j=st-1,k=en+1;
		for (i=st;i<=en;++i)
			if (beg[p[i]]<=mid && !(beg[p[i]]<=l && r<=end[p[i]]))//将左端点小于等于mid的放入新边集中计算
				tmp[++j]=p[i];
			else
				tmp[--k]=p[i];
		memcpy(p+st,tmp+st,sizeof(int)*(en-st+1));
		dfs(l,mid,st,j);
		j=st-1,k=en+1;
		for (i=st;i<=en;++i)
			if (end[p[i]]>mid && !(beg[p[i]]<=l && r<=end[p[i]]))
				tmp[++j]=p[i];
			else
				tmp[--k]=p[i];
		memcpy(p+st,tmp+st,sizeof(int)*(en-st+1));
		dfs(mid+1,r,st,j);
	}
	for (i=st;i<=en;++i)//还原
		if (bz[p[i]]){
			int t=bz[p[i]];
			siz[fa[t]]-=siz[t];
			fa[t]=t;
			col[t]=0;
			bz[p[i]]=0; 
		}
}
int main(){
	freopen("graph.in","r",stdin);
	freopen("graph.out","w",stdout);
	scanf("%d%d",&V,&n);
	for (int i=1;i<=n;++i){
		int op;
		scanf("%d",&op);
		if (op){
			int u,v;
			scanf("%d%d",&u,&v);
			u++,v++;
			e[++m]={u,v};
			beg[m]=i,end[m]=n;
		}
		else{
			int x;
			scanf("%d",&x);
			end[x+1]=i-1;
		}
	}
	for (int i=1;i<=m;++i)
		p[i]=i;
	for (int i=1;i<=V;++i)
		fa[i]=i,siz[i]=1,col[i]=0;
	dfs(1,n,1,m);
	for (int i=1;i<=n;++i)
		if (ans[i])
			printf("YES\n");
		else
			printf("NO\n");
	return 0;
}

下面这个是LCT做法(调了我好久啊……):

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define N 300010
struct Node *null;
struct Node{//以下是LCT的模板
	Node *fa,*c[2];
	bool is_root,rev;
	int end,siz;
	Node *mn;
	inline bool getson(){return fa->c[0]!=this;}
	inline void reserve(){
		swap(c[0],c[1]);
		rev^=1;
	}
	inline void pushdown(){
		if (rev){
			c[0]->reserve();
			c[1]->reserve();
			rev=0;
		}
	}
	void push(){
		if (!is_root)
			fa->push();
		pushdown();
	}
	inline void update(){
		siz=c[0]->siz+c[1]->siz+1;
		mn=this;
		if (c[0]->mn->end<mn->end)
			mn=c[0]->mn;
		if (c[1]->mn->end<mn->end)
			mn=c[1]->mn;
	}
	inline void rotate(){
		Node *y=fa,*z=y->fa;
		if (y->is_root){
			y->is_root=0;
			is_root=1;
		}
		else
			z->c[y->getson()]=this;
		bool k=getson();
		fa=z;
		y->c[k]=c[k^1];
		c[k^1]->fa=y;
		c[k^1]=y;
		y->fa=this;
		siz=y->siz,mn=y->mn;
		y->update();
	}
	inline void splay(){
		push();
		while (!is_root){
			if (!fa->is_root){
				if (getson()!=fa->getson())
					rotate();
				else
					fa->rotate();
			}
			rotate();
		}
	}
	inline Node *access(){
		Node *x=this,*y=null;
		for (;x!=null;y=x,x=x->fa){
			x->splay();
			x->c[1]->is_root=1;
			x->c[1]=y;
			y->is_root=0;
			x->update();
		}
		return y;
	}
	inline void mroot(){
		access()->reserve();
	}
	inline void link(Node *y){
		y->mroot();
		y->splay();
		y->fa=this;
	}
	inline void cut(Node *y){
		mroot();
		y->access();
		splay();
		c[1]->fa=null;
		c[1]->is_root=null;
		c[1]=null;
		update();
	}
} d[N],e[N];//处理边的常用套路:将边化为点处理
int V,n,m;
struct edge{
	int u,v;
} ed[N];
int o[N];
bool bz[N];
int main(){
	freopen("graph.in","r",stdin);
	freopen("graph.out","w",stdout);
	null=d;
	*null={null,null,null,0,0,2147483647,0,null};
	scanf("%d%d",&V,&n);
	for (int i=1;i<=V;++i)
		d[i]={null,null,null,1,0,2147483647,1,&d[i]};
	for (int i=1;i<=n;++i){
		int op;
		scanf("%d",&op);
		if (op){
			++m;
			scanf("%d%d",&ed[m].u,&ed[m].v);
			ed[m].u++,ed[m].v++;
			e[m]={null,null,null,1,0,n,1,&e[m]};
			o[i]=m;
		}
		else{
			int x;
			scanf("%d",&x);
			x++;
			e[x].end=i-1;
			o[i]=-x;
		}
	}
	for (int i=1,j=0;i<=n;++i){
		if (o[i]>0){
			int u=ed[o[i]].u,v=ed[o[i]].v;
			d[u].mroot(),d[u].splay();//这个操作仅仅是为了判断它们是否连通,作为一个懒人,我不想算出它们的根来比较。
			d[v].mroot(),d[v].splay();
			if (d[u].fa!=null){
				Node *p=d[u].access(),*q=p->mn;
				int len=p->siz+1>>1;//LCT中有边又有点,所以要处理一下
				if (e[o[i]].end<q->end)
					q=&e[o[i]];
				else{
					q->cut(&d[ed[int(q-e)].u]);
					q->cut(&d[ed[int(q-e)].v]);
					bz[int(q-e)]=1;
					d[u].link(&e[o[i]]);
					e[o[i]].link(&d[v]);
				}
				if (len&1)
					j=max(j,q->end);
			}
			else{
				d[u].link(&e[o[i]]);
				e[o[i]].link(&d[v]);
			}
		}
		else{
			if (!bz[-o[i]]){
				bz[-o[i]]=1;
				Node *q=&e[-o[i]];
				q->cut(&d[ed[-o[i]].u]);
				q->cut(&d[ed[-o[i]].v]);
			}
		}
		if (i<=j)
			printf("NO\n");
		else
			printf("YES\n");
	}
	return 0;
}

总结

这应该可以成为处理“动态图”类型题的一个很好的套路。
只要可以离线,就处理删除时间。
转化成“先进后出”,或者搞最大生成树。

转载于:https://www.cnblogs.com/jz-597/p/11145233.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值