题目
描述
题目大意很明确了,所以不说……
思考历程
一看见这题,咦,这就是传说中的动态图吗?
普通的动态图是维护连通性,这题是维护它是否是二分图,换言之就是维护它是否有奇环。
好像很复杂的样子。
想用LCT搞一搞,但是搞了很久终究搞不出来。
如果这道题全部都是加入就好了,但对于删除,好像要影响很多东西……
想了很久终将放弃。
正解
现在主要的正解大体分为两种:
第一种方法是使用线段树。
对于每一条边,预处理除它们的加入和删除时间。
可以把它们存在的时间看作时间轴上的一段区间。
然后就有个很强大的做法:将这条边塞入线段树中,也就是代表这段区间的线段树上个节点上。
将所有的边加进去,然后顺序遍历。对于每个节点,进入它时,将挂在它上面的所有边加入某个数据结构中;从它回到父亲时,将这些边从那个数据结构中删除。
这个数据结构用来维护它是否是二分图,我们可以用可持久化并查集来实现。
在线段树上一直往下走的过程中,可以通过当前的加入操作来判断它是否出现了奇环。如果没有出现,继续做下去;如果出现了,那么这个节点代表的区间的答案都是NO
,直接记下,下面的就不用做了,直接回去。
显然每条边在线段树上挂的节点有个,每次对可持久化并查集的操作为的时间,所以时间复杂度是.。
可以通过。
那么这种做法的优点在哪里?
如果是按照原来的方式从左到右计算,那么很难处理出来。
会经常遇到这种情况:先加入,再加入,到后面先出来,然后再出来。出来后会对产生影响,所以比较难处理。
如果先出来,再出来,那就比较好处理了。在后进一次又出一次,对于之前的没有什么影响,所以继续处理比较方便。
总结一下,如何处理得方便呢?就是让操作变成先入后出的模式。
将其转化成先入后出的模式,线段树无疑是个非常好的选择。因为先入后出让我们想到了栈,从而想到树的遍历。将操作转化成树的形式,自然就要用到线段树了。
类似的方法还有分治。
实际上分治和线段树的做法在本质上是一模一样的,只不过实现方式不同。
将分治的那棵树画出来,其实那就是一棵线段树。
每次处理的时候,先将当前边集中的所有边扫一遍,完全覆盖整个区间的就加入可持久化并查集中,然后在区间中取个中点,将左端点在中点左边的边加入新边集中,递归下去做,右边同理。
它只是将所有边一起处理罢了,时间复杂度是一样的。
第二种做法比较高级,是用LCT维护的。
还是要将每条边的删除时间处理出来。
然后在做的过程中,维护一个标记表示当前到的答案都为NO
。
还有维护一棵关于删除时间的最大生成树。
加边的时候,如果两个端点之间不连通,就连接两点。
否则截取两点之间的路径,找出这个环中删除时间最早的边(包括这条新加进去的边)。
如果这个环是奇环,就用这个时间更新一下。
将删除时间最早的边删去,加入这条边(如果被删的是这条边就不用加了)。
删边的时候,如果这个边被删过了,那就不删,否则就将其删了。
一直这么做下去就好。
时间复杂度是,不要忘记乘上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