线段树分治从入门到入门
Halberd_Cease · · 题解
从这个线段树分治板子题,从头整理一下线段树分治的一些重点。
首先看到本题,如果只有添加边一种操作的话,显然是可以用扩展域并查集维护的,如果不会可以翻到本文最后面有讲。
但是本题有删除边的操作,这就不能只用并查集维护了。
我们考虑一个
考虑这个暴力不足的地方:由于操作是对一个时间区间都存在,有一些边在下一个询问中仍然是存在的,但是我们将其全部删掉了。我们的并查集虽然不能支持删除边,但是可以撤销(可撤销并查集也见文末)!如果我们知道相对下一个询问,我们需要撤销哪些边,再加上哪些边,是不是可以优化复杂度呢?
线段树分治的思想
我们对时间(询问)建立一颗线段树,树的节点存的信息是【覆盖了这个区间的操作】。
考虑遍历整颗线段树,维护一颗可撤销并查集。当进入一个线段树的子节点时,在并查集上添加覆盖这个区间的边。然后继续递归子节点,直到叶子节点(对应的就是一个询问),统计对应的答案。回溯时,将这个节点加的边在并查集上撤销。
这样,递归到每一个叶子节点时,并查集中的边都是这个询问的完整状态。
分析复杂度,遍历线段树的时间是
这就是线段树分治。
看到什么应该想到线段树分治?
-
多个操作,每一个操作的影响范围是一个区间(时间区间,询问区间)。
-
多个询问,询问(某个时间时,某个操作后的)答案。
比如本题:操作为删除或者添加边,也就是说一条边的影响范围是一个区间。同时在每一次操作后求答案(是否为二分图)。
实现
#include<bits/stdc++.h>
using namespace std;
struct BCJ //可撤销并查集
{
int n;
int fa[200010];
int dep[200010];
void init(int x)
{
n=x;
for(int i=1;i<=2*n;i++)
{
fa[i]=i;
dep[i]=1;
}
}
int find(int x)
{
return x==fa[x]?x:find(fa[x]);
}
void merge(int x,int y,stack<int>&sta)
{
x=find(x),y=find(y);
if(x==y)return;
if(dep[x]<dep[y])swap(x,y);
fa[y]=x;
dep[x]=max(dep[x],dep[y]+1);
sta.push(y);
}
void roll_back(stack<int>&sta)
{
while(!sta.empty())
{
int x=sta.top();
sta.pop();
fa[x]=x;
}
}
}bcj;
struct EDGE
{
int x,y;
}edge[200010];
vector<int> tr[800010];
int n,m;
bitset<200010>ans;
void update(int now,int l,int r,int ll,int rr,int e)
{
if(l>rr||r<ll)return ;
if(ll<=l&&rr>=r)
{
tr[now].push_back(e); //操作标记
return ;
}
int mid=l+r>>1;
update(now<<1,l,mid,ll,rr,e);
update(now<<1|1,mid+1,r,ll,rr,e);
}
void getans(int now,int l,int r)
{
stack<int>sta;
for(auto x:tr[now]) //加边
{
if(bcj.find(edge[x].x)==bcj.find(edge[x].y))
{
for(int i=l;i<=r;i++)
{
ans[i]=0;
}
bcj.roll_back(sta);
return ;
} //剪枝,如果在边没加完就已经冲突了,再加边也不可能成为二分图,于是整个区间都不可能成为答案。
bcj.merge(edge[x].x,edge[x].y+n,sta);
bcj.merge(edge[x].x+n,edge[x].y,sta);
}
if(l!=r) //非叶子节点继续递归
{
int mid=l+r>>1;
getans(now<<1,l,mid);
getans(now<<1|1,mid+1,r);
}
bcj.roll_back(sta);
}
map<pair<int,int>,int>mp;
int cnte=0;
signed main()
{
ans.set();
cin>>n>>m;
bcj.init(n);
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
if(mp[{x,y}])
{
int from=mp[{x,y}];
cnte++;
edge[cnte].x=x,edge[cnte].y=y;
update(1,1,m,from,i-1,cnte);
mp[{x,y}]=0;
}
else
{
mp[{x,y}]=i;
}
}
for(auto x:mp)
{
if(x.second)
{
cnte++;
edge[cnte].x=x.first.first,edge[cnte].y=x.first.second;
update(1,1,m,x.second,m,cnte);
}
}
getans(1,1,m);
for(int i=1;i<=m;i++)
{
cout<<(ans[i]?"YES\n":"NO\n");
}
}
不知道谁的线段树分治题单,希望有用。
扩展域并查集
有很多形式,这里只讲维护二分图的。
考虑对于每一个真实的点,建立【反点】,并查集上点
真实的图中连一条边,说明两个点不能在一个部中,这时分别给对方的反点在并查集上连边,完成操作。
如果一次操作连接点
可撤销并查集
考虑并查集操作:把
撤销需要按操作的顺序来,也就是说需要用栈将进行的操作存下来,需要撤销的时候直接弹栈,令
具体可以参考上面的代码理解。