[JZOJ4769]【GDOI2017模拟9.9】graph
题目
描述
题目大意很明确了,所以不说……
思考历程
一看见这题,咦,这就是传说中的动态图吗?
普通的动态图是维护连通性,这题是维护它是否是二分图,换言之就是维护它是否有奇环。
好像很复杂的样子。
想用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;
}
总结
这应该可以成为处理“动态图”类型题的一个很好的套路。
只要可以离线,就处理删除时间。
转化成“先进后出”,或者搞最大生成树。