其实很好理解的一个算法:我们把所有操作放到一棵树里,然后像遍历树一样遍历整棵树。有点像把操作放到dfs树里然后做,回溯的时候把操作还原。然而这个算法不一样的是:一个操作可能会覆盖线段树的多个节点,如果一个操作横跨当前两个节点我们可能需要把它拆成两个操作。当然对于线段树熟悉的话,这并不是问题,一个操作最多被分成
log
l
o
g
个。
我们模仿线段树,如果这个操作覆盖整个区间,就操作,否则看它是否横跨两个儿子,如果是,则分成两个,否则直接分给对应儿子。
下面看一道例题:4025: 二分图
Description
神犇有一个 n n 个节点的图。因为神犇是神犇,所以在时间内一些边会出现后消失。神犇要求出每一时间段内这个图是否是二分图。这么简单的问题神犇当然会做了,于是他想考考你。
Input
输入数据的第一行是三个整数
n
n
,,
T
T
。
第2行到第+1行,每行4个整数
u
u
,,
start
s
t
a
r
t
,
end
e
n
d
。第
i
i
+1行的四个整数表示第条边连接
u
u
,两个点,这条边在
start
s
t
a
r
t
时刻出现,在第
end
e
n
d
时刻消失。
Output
输出包含 T T 行。在第行中,如果第 i i 时间段内这个图是二分图,那么输出“”,否则输出“ No N o ”,不含引号。
解:
我们首先要知道怎么判断二分图。判断连通性?自然想到了并查集。具体怎么做,在网上找了好久居然没有找到?
然后大佬表示:这不是很简单吗?我们对于每个点记录它到它父亲路径长度的奇偶性。如果当前只有一个环的话(如图):
观察两个绿色的点,因为两个点到最上面那个点都会走一条共同路径,这条路径走了两次,所以对奇偶性是没有影响的。
然后我们考虑怎么连接两个点?
观察两个绿点,我们是要把绿点连起来,但实际上我们是要把并查集两个祖先连起来。两个绿点的路径长应该是奇数,所以我们把蓝箭头的路径求出在异或一下就可以得到这条边的权。
那么我们需不需要更改路径上的权呢?事实上是不需要的。我们观察蓝色绿色黄色组成的环,他们异或起来应该是0,所以我们从下面走和从上面走,两个加在一起是一整个环,所以上下走的奇偶性是相同的。
至于要修改回去的吧,我们不能路径压缩,这样就不能修改回去了。我们并查集要启发式合并一下。
然后按照线段树分治的方法做一下就好了。
这里有一个问题,我们为了好写,把一个区间的修改全部放进一个vector里,直接递归传参vector。感觉空间有点爆炸?
应该是可以记录两个儿子需要的修改,然后这两个会有重叠,应该是可以实现的?但是可能就难写得多。
这里只给出空间的做法。(一直感觉vector不优美)
code:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<cstdlib>
#include<ctime>
using namespace std;
struct lxy{
int s,t,x,y;
bool operator < (const lxy &QAQ)const{
if(s!=QAQ.s) return s<QAQ.s;
return t>QAQ.t;
}
}data[200005];
struct lxy1{
int x,y;
}yyy;
int n,m,T,fa[100005],k;
bool ans[100005],dis[100005];
int findit(int u){
while(fa[u]!=u){
k^=dis[u];
u=fa[u];
}
return u;
}
int readit()
{
int a=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9'){
a=a*10+ch-'0';
ch=getchar();
}
return a;
}
void CDQ(int l,int r,vector <lxy> &v)
{
int mid=(l+r)>>1;
vector <lxy> t1,t2;//两儿子需要的操作
queue <lxy1> d;//记录操作,用于还原
for(int i=v.size()-1;i>=0;i--){//包含整个区间就做
if(v[i].s<=l&&v[i].t>=r){
k=0;int x=findit(v[i].x),y=findit(v[i].y);
if(x==y&&k==0){//出现奇环这个儿子下面全是No,可以直接回溯return
for(int j=l;j<=r;j++) ans[j]=1;
while(!d.empty()){
lxy1 now=d.front();
fa[now.x]=now.x;dis[now.x]=0;//开始还原的时候fa赋成0了,还不知道怎么RE的
d.pop();
}
return;
}
else if(x!=y){
if((rand()&1)==0){
fa[x]=y;dis[x]=(k^1);
yyy.x=x;yyy.y=y;
d.push(yyy);
}
else{
fa[y]=x;dis[y]=(k^1);
yyy.x=y;yyy.y=x;
d.push(yyy);
}
}
}
else if(v[i].t<=mid) t1.push_back(v[i]);//给左儿子
else if(v[i].s>mid) t2.push_back(v[i]);//给右儿子
else t1.push_back(v[i]),t2.push_back(v[i]);横跨就两个都给
}
if(l!=r) CDQ(l,mid,t1),CDQ(mid+1,r,t2);
while(!d.empty()){//还是最后要回溯
lxy1 now=d.front();
fa[now.x]=now.x;dis[now.x]=0;
d.pop();
}
}
int main()
{
srand(2333);
scanf("%d%d%d",&n,&m,&T);
for(int i=1;i<=n;i++) fa[i]=i;
vector <lxy> v;
for(int i=1;i<=m;i++)
data[i].x=readit(),data[i].y=readit(),data[i].s=readit(),data[i].t=readit(),data[i].s++;
for(int i=1;i<=m;i++)
if(data[i].s<=data[i].t)
v.push_back(data[i]);
CDQ(1,T,v);
for(int i=1;i<=T;i++)
{
if(ans[i]==1) printf("No\n");
else printf("Yes\n");
}
}