题目

描述

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


思考历程

一看见这题,咦,这就是传说中的动态图吗?

普通的动态图是维护连通性,这题是维护它是否是二分图,换言之就是维护它是否有奇环。

好像很复杂的样子。

想用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再出来,那就比较好处理了。BBB在AAA后进一次又出一次,对于之前的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;
}

总结

这应该可以成为处理“动态图”类型题的一个很好的套路。

只要可以离线,就处理删除时间。

转化成“先进后出”,或者搞最大生成树。

[JZOJ4769]【GDOI2017模拟9.9】graph的更多相关文章

  1. 【NOIP模拟题】Graph(tarjan+dfs)

    似乎我搞得太复杂了? 先tarjan缩点然后dfs就行了QAQ. (我不说我被一个sb错调了半个小时....不要以为缩点后dfs就可以肆无忌惮的不加特判判vis了.. bfs的做法:减反图,然后从大到 ...

  2. jzoj4918. 【GDOI2017模拟12.9】最近公共祖先 (树链剖分+线段树)

    题面 题解 首先,点变黑的过程是不可逆的,黑化了就再也洗不白了 其次,对于\(v\)的祖先\(rt\),\(rt\)能用来更新答案当且仅当\(sz_{rt}>sz_{x}\),其中\(sz\)表 ...

  3. jzoj4915. 【GDOI2017模拟12.9】最长不下降子序列 (数列)

    题面 题解 调了好几个小时啊--话说我考试的时候脑子里到底在想啥-- 首先,这个数列肯定是有循环节的,而且循环节的长度\(T\)不会超过\(D\) 那么就可以把数列分成三份,\(L+S+R\),其中\ ...

  4. jzoj4916. 【GDOI2017模拟12.9】完全背包问题 (背包+最短路)

    题面 题解 考场上蠢了--这么简单的东西都想不到-- 首先排序加去重. 先来考虑一下,形如 \[a_1x_1+a_2x_2+...a_nx_n=w,a_1<a_2<...<a_n,x ...

  5. 【GDOI2017模拟12.9】最近公共祖先

    题目 分析 首先,将这些节点按dfs序建一棵线段树. 因为按dfs序,所以在同一子树上的节点会放在线段树相邻的位置. 发现,对于一个位置x,它的权值只会对以x为根的子树造成影响. 当修改x时,用w[x ...

  6. [JZOJ4665] 【GDOI2017模拟7.21】数列

    题目 题目大意 给你一个数列,让你找到一个最长的连续子序列,满足在添加了至多KKK个数之后,能够变成一条公差为DDD的等差数列. 思考历程 一眼看上去似乎是一道神题-- 没有怎么花时间思考,毕竟时间都 ...

  7. [JZOJ4913] 【GDOI2017模拟12.3】告别

    题目 描述 题目大意 给你两个排列AAA和BBB,每次随即选三个数进行轮换操作,问mmm次操作内使AAA变成BBB的概率. 思考历程 首先随便搞一下,就变成了AAA中每个数回归自己原位. 一眼望去,感 ...

  8. [JZOJ4633] 【GDOI2017模拟7.15】萌萌哒

    题目 描述 题目大意 给你一个数列,接下来有许多个操作,使得区间[l1,r1][l_1,r_1][l1​,r1​]和[l2,r2][l_2,r_2][l2​,r2​]对应的位置染上同样的颜色(使得它们 ...

  9. [JZOJ4640] 【GDOI2017模拟7.15】妖怪

    题目 描述 题目大意 给你一堆aia_iai​和bib_ibi​(方便起见用的变量和上面不一样),让你搞出一个xxx(相当于题目中的ba\frac{b}{a}ab​,随便推推就能知道), 使得max⁡ ...

随机推荐

  1. Rootkit之SSDT hook(通过CR0)

    CR0当中有一个写保护位,是保护内存不可写属性的,为了能够写入内核,只能把它的保护给咔嚓掉了,不过--如果做完了手脚但不还原写保护属性的话,极有可能会BOSD. /================== ...

  2. Java-Class-@I:org.apache.ibatis.annotations.Mapper

    ylbtech-Java-Class-@I:org.apache.ibatis.annotations.Mapper 1.返回顶部   2.返回顶部 1. package com.ylbtech.ed ...

  3. console.log("正常-普天数据已调用");

    console.log("正常-普天数据已调用");

  4. 记一次java简单的if语句使用多态重构

    场景描述: 一个controller中,部门领导有布置任务,查看任务整体情况,查看部门成员,查看部门成员完成情况,导出任务详情,如下: @RestController @RequestMapping( ...

  5. C++之关键字&标识符命名规则

    关键字 **作用:**关键字是C++中预先保留的单词(标识符) * **在定义变量或者常量时候,不要用关键字** C++关键字如下: 提示:在给变量或者常量起名称时候,不要用C++得关键字,否则会产生 ...

  6. USACO2007 Protecting the Flowers /// 比值 前缀和 oj21161

    题目大意: 有N (2 ≤ N ≤ 100,000) 头牛偷吃花 将牛赶回牛棚需Ti minutes (1 ≤ Ti ≤ 2,000,000) 每头牛每分钟能吃Di (1 ≤ Di ≤ 100) 朵花 ...

  7. Linux 实用指令(9)--进程管理

    目录 进程管理 1 进程的基本介绍 2 显示系统执行的进程 2.1 说明: 2.2 ps指令详解 2.3 应用实例 3 终止进程kill和killall 3.1 介绍 3.2 基本语法 3.3 常用选 ...

  8. arm-linux-strip 的使用

    3.2.1    1. 移除所有的符号信息 [arm@localhost gcc]#cp hello hello1 [arm@localhost gcc]#arm­linux­strip ­strip ...

  9. arc098D Xor Sum 2

    题意:给你一个数列,问有多少对(l,r)满足A[l]+A[l+1]+...+A[r]=A[l]^A[l+1]^...^A[r]? 标程: #include<bits/stdc++.h> u ...

  10. Windows 设置内网和外网同时使用

    想要电脑同时使用内网和外网必须具备两个网卡,一个是无线网卡一个是本地连接,无线网卡用来连接wifi也就是外网,而本地连接需要网线连接内网,外网是不需要做设置的,我们只需要设置内网即可,鼠标右击电脑右下 ...