线段树分治学习笔记
线段树分治学习笔记
思想
对于下面这样一类问题
在一个时间轴上,有一系列有时间区间的操作,且有询问某个时间点上所有操作的贡献的一些询问
我们可以采用线段树分治,就是在时间轴上建立一棵线段树,把操作放在线段树的区间上,然后遍历线段树,执行操作,统计贡献,当递归到叶子节点时回答询问,回溯时再撤销操作.
最开始听不会太懂,做几道题基本上就会懂了
例题
线段树分治上维护数据结构有两个思路,一个是利用操作序变成树上dfs序也就是栈序,使用可撤销数据结构;另一个是利用树深度有限多次复用数据结构,具体来说就是维护 log 个数据结构,每次往下走都从父亲那里拷贝一份过来
by 夏色祭Official in luogu
不过现在只刷了可撤销并查集呢
1 P5787 二分图 /【模板】线段树分治
题目大意是在长度为k的时间段里,n个点的图上有m条边,其中边 \(i\) 的存在时间为\(l_i-r_i\) ,求每个单位时间内图是否为二分图
判断图是否为二分图就是判断有没有奇环,这个可以用并查集以类似于食物链的方法来解决。在时间轴上建立并查集,对于存在一段时间的边,我们可以把它挂在线段树\(l_i-r_i\)的一些最大组成节点上,在回溯时用可撤销的并查集(按秩合并)来撤销加的边,走到一个节点判断图是否为奇环,最后到叶子节点输出答案即可。
注意若在一个节点已判断有奇环,则其子节点不用在进行操作,否则被 hack 掉
结合代码理解
#include<bits/stdc++.h>
using namespace std;
int const MAXN=2e5+10;
int n,m,k,cnt,tot,tott,_;
int f[MAXN],siz[MAXN];
vector<int>seg[MAXN<<2];
stack<int>st;
struct op{
int u,v,be,en;
}e[MAXN];
void change(int x,int l,int r,int L,int R,int a){
if(l<=L && R<=r){
seg[x].push_back(a);
return;
}
int mid=(L+R)>>1;
if(l<=mid)change(x<<1,l,r,L,mid,a);
if(r>mid)change(x<<1|1,l,r,mid+1,R,a);
return ;
}
int get(int x){
while(x!=f[x])x=f[x];
return x;
}
bool merge(int a,int b){
bool flag=1;
int fa=get(a),fb=get(b),fan=get(a+n),fbn=get(b+n);
if(fa==fan || fb==fbn || fa==fb || fan==fbn)return 0;
if(fa!=fbn){
if(siz[fa]>siz[fbn])swap(fa,fbn);
f[fa]=fbn;siz[fbn]+=siz[fa];st.push(fa);
}
if(fb!=fan){
if(siz[fb]>siz[fan]) swap(fb,fan);
f[fb]=fan;siz[fan]+=siz[fb];st.push(fb);
}
return 1;
}
void del(int sum){
while(sum<st.size()){
int x=st.top();st.pop();
siz[f[x]]-=siz[x];f[x]=x;
}
return;
}
void query(int x,int L,int R,bool flag){
int sum=st.size();
for(int i=0;i<seg[x].size();i++){
if(!flag)break;
int a=e[seg[x][i]].u,b=e[seg[x][i]].v;
if(!merge(a,b))flag=0;
}
if(L==R){
if(flag)printf("Yes\n");
else printf("No\n");
del(sum);
return;
}
int mid=(L+R)>>1;
query(x<<1,L,mid,flag);query(x<<1|1,mid+1,R,flag);
del(sum);
return;
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=2*n;i++)f[i]=i,siz[i]=1;
for(int i=1;i<=m;i++){
int x,y,l,r;
scanf("%d%d%d%d",&x,&y,&l,&r);
if(l==r)continue;
e[i]=(op){x,y,l+1,r};
change(1,l+1,r,1,k,i);
}
query(1,1,k,1);
return 0;
}
2 P5214 [SHOI2014]神奇化合物
跟例1差不多的思路,挺板的。把边挂在线段树上,用可撤销的并查集维护分子关系
#include<map>
#include<set>
#include<cmath>
#include<queue>
#include<cstdio>
#include<vector>
#include<climits>
#include<string>
#include<deque>
#include<bitset>
#include<cstring>
#include<stack>
#include<cstdlib>
#include<iostream>
#include<ctime>
#include<algorithm>
using namespace std;
int const MAXN=4e5+10;
int n,m,tot,tott,cnt,_,q,ans;
int e[5001][5001],f[MAXN],siz[MAXN];
vector<int>seg[MAXN*4];
stack<int>sta;
struct edge{
int a,b,st,en;
}op[MAXN];
int get(int x){
while(x!=f[x])x=f[x];
return x;
}
void insert(int x,int l,int r,int L,int R,int id){
if(l<=L && R<=r){
seg[x].push_back(id);
return;
}
int mid=(L+R)>>1;
if(l<=mid)insert(x<<1,l,r,L,mid,id);
if(r>mid)insert(x<<1|1,l,r,mid+1,R,id);
return ;
}
void merge(int a,int b){
int fa=get(a),fb=get(b);
if(fa!=fb){
if(siz[fa]>siz[fb])swap(fa,fb);
f[fa]=fb;siz[fb]+=siz[fa];
sta.push(fa);ans--;
}
}
void del(int sum){
while(sum<sta.size()){
int x=sta.top();sta.pop();
ans++;
siz[f[x]]-=siz[x];f[x]=x;
}
}
void query(int x,int L,int R){
int sum=sta.size();
for(int i=0;i<seg[x].size();i++){
int a=op[seg[x][i]].a,b=op[seg[x][i]].b;
merge(a,b);
}
if(L==R){
printf("%d\n",ans);
del(sum);
return;
}
int mid=(L+R)>>1;
query(x<<1,L,mid);query(x<<1|1,mid+1,R);
del(sum);
return;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
f[i]=i,siz[i]=1;
}
for(int i=1;i<=m;i++){
cnt++;
scanf("%d%d",&op[cnt].a,&op[cnt].b);
e[op[cnt].a][op[cnt].b]=e[op[cnt].b][op[cnt].a]=cnt;
op[cnt].st=1,op[cnt].en=0;
}
scanf("%d",&q);
for(int i=1;i<=q;i++){
char c[10];int a,b;
scanf("%s",c+1);
if(c[1]=='A'){
scanf("%d%d",&a,&b);
e[a][b]=e[b][a]=++cnt;
op[cnt].a=a,op[cnt].b=b;
op[cnt].st=tot+1,op[cnt].en=0;
}else if(c[1]=='D'){
scanf("%d%d",&a,&b);
op[e[a][b]].en=tot;
}else if(c[1]=='Q')tot++;
}
for(int i=1;i<=cnt;i++){
if(op[i].en==0)op[i].en=tot;
insert(1,op[i].st,op[i].en,1,tot,i);
}
ans=n;
query(1,1,tot);
return 0;
}
3 CF576E Painting Edges
首道CF黑题!
这道题是另外一道题的加强版P5787 二分图 /【模板】线段树分治
现在问题在于如何维护修改,以及修改之后边的颜色的问题
考虑在在一条边上有两次修改\(a,b\),则在\((a+1,b-1)\)的时间内颜色要么是\(a\)修改的颜色,要么是\(a\)之前的一次修改的颜色(或无色),那么我们在a时刻假设修改成功,即可在那一时刻判断这次操作是否合法,决定\((a+1,b-1)\)这段时间这条边的颜色
所以现在我们需要维护一个支持撤销的并查集,于是选择按秩合并的并查集.边的颜色又会存在一段时间,便可以用线段树分治来维护
时间复杂度\(O(mlog_2nlog_2q)\)
#include<map>
#include<set>
#include<cmath>
#include<queue>
#include<cstdio>
#include<vector>
#include<climits>
#include<string>
#include<deque>
#include<bitset>
#include<cstring>
#include<stack>
#include<cstdlib>
#include<iostream>
#include<ctime>
#include<algorithm>
using namespace std;
int const MAXN=5e5+10;
int n,m,k,q,root,ans;
int f[60][MAXN<<1],vis[MAXN],siz[60][MAXN<<1],last[MAXN],cnt[MAXN],sati[MAXN];
vector<int>vec[MAXN<<2];
struct edge{
int u,v;
}e[MAXN];
struct operation{
int ei,ci,t;
}op[MAXN];
stack<pair<int,int> >S;
inline int get(int c,int x){
while(x!=f[c][x])x=f[c][x];
return x;
}
void merge(int c,int x,int y){
int fx=get(c,x),fy=get(c,y),fxn=get(c,x+n),fyn=get(c,y+n);
if(fx==fyn || fxn==fy)return;
//if(cnt[c]==2*n){ans+=sati[c];}
//cnt[c]-=2;
if(siz[c][fx]>siz[c][fyn])swap(fx,fyn);
siz[c][fyn]+=siz[c][fx],f[c][fx]=fyn,S.push(make_pair(fx,c));
if(siz[c][fy]>siz[c][fxn])swap(fy,fxn);
siz[c][fxn]+=siz[c][fy],f[c][fy]=fxn;S.push(make_pair(fy,c));
}
void insert(int x,int L,int R,int l,int r,int k){
if(l<=L && R<=r){vec[x].push_back(k);return;}
int mid=(L+R)>>1;
if(l<=mid)insert(x<<1,L,mid,l,r,k);
if(r>mid)insert(x<<1|1,mid+1,R,l,r,k);
return;
}
void query(int x,int L,int R){
int tag=S.size();
for(int i=0;i<vec[x].size();i++){
int id=vec[x][i];
if(op[id].ci)merge(op[id].ci,e[op[id].ei].u,e[op[id].ei].v);
}
if(L==R){
int u=e[op[L].ei].u,v=e[op[L].ei].v,c=op[L].ci;
u=get(c,u),v=get(c,v);
if(u==v){
printf("NO\n");
op[L].ci=last[op[L].ei];
}else{
last[op[L].ei]=c;
printf("YES\n");
//if(c)merge(c,u,v);
//printf("%d %d %d\n",c,cnt[c],ans);
}
}else{
int mid=(L+R)>>1;
query(x<<1,L,mid);
query(x<<1|1,mid+1,R);
}
while(tag<S.size()){
int x=S.top().first,c=S.top().second;S.pop();
siz[c][f[c][x]]-=siz[c][x];f[c][x]=x;
//if(++cnt[c]==2*n)ans-=sati[c];
}
}
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int main(){
root=1;
n=read(),m=read(),k=read(),q=read();
for(int i=1;i<=n;i++){
cnt[i]=2*n;
for(int c=1;c<=k;c++){
f[c][i]=i,f[c][i+n]=i+n;
siz[c][i]=siz[c][i+n]=1;
}
}
for(int i=1;i<=m;i++){
e[i].u=read(),e[i].v=read();
last[i]=q+1;
}
//for(int i=1;i<=k;i++)sati[i]=read();
for(int i=1;i<=q;i++){
op[i].ei=read(),op[i].ci=read();
}
for(int i=q;i>=1;i--){
int ei=op[i].ei;
if(i<last[ei]-1){
insert(root,1,q,i+1,last[ei]-1,i);
last[ei]=i;
}
}
memset(last,0,sizeof(last));
query(root,1,q);
}
参考资料
- xht37 线段树分治 学习笔记
- FlashHu 线段树分治总结(线段树分治,线段树,并查集,树的dfn序,二分图染色)
- 夏色祭Official在Luogu讨论中的发言