线段树分治

概念

线段树分治这种算法主要用于解决操作在一段时间区间内有效,每次询问某时刻的信息的一类问题

主要思想是以时间为下标建立线段树,这样就可以将在 \([l,r]\) 时间内生效的操作记录在线段树上,对于询问就可以直接从根节点开始遍历这棵线段树,每个节点的操作直接执行,回溯时撤销操作,这样就可以在优秀的时间复杂度里解决问题

光说概念还是有点抽象,下面举点例子就能理解了,其实非常简单

例子1

传送门Luogu P5787 二分图 /【模板】线段树分治

一句话题意 : 一张有 \(n\) 个节点的图,在总计 \(k\) 的时间里会出现 \(m\) 条边,其中第 \(i\) 条边连接节点 \(x_i\)\(y_i\) 且在 \(l_i\) 时刻出现,在 \(r_i\) 时刻消失,对于所有的 \(i\in[1,k]\) ,求在第 \(i\) 个时间段内这张图是否是一个二分图

数据范围\(n,k = 10^5\)\(m = 2\times 10^5\)\(1 \le x,y \le n\)\(0 \le l \le r \le k\)

题解

看到第 \(i\) 条边在 \(l_i\) 时刻出现,在 \(r_i\) 时刻消失且询问均为询问第 \(i\) 个时间段内这张图是否是一个二分图就可以想到线段树分治

我们建立一棵下标为 \([1,k]\) 的线段树,每一个节点上挂一个 \(\text{vector}\),维护在这段时间区间上有哪些边是有效的,把有效边的编号放进去,每条边所在的时间区间最多会被分割为 \(\log k\)

对于询问,我们需要找到一种方法能方便地判断一张图是不是二分图,且支持加边删边操作,不难想到利用 并查集(扩展域并查集)

每加入一条新的边就判断这条边连接的两个节点是否已经在同一个并查集中,如果已经在了就说明不合法,如果不在就把两个节点分别与对面的敌对节点连边( \(x\) 不能与 \(y\) 在一个并查集中,那 \(x\) 就必定与所有“不能与 \(y\) 在一个并查集中”的节点在同一个并查集中),这样就可以方便地维护图是否为二分图(可以通过 Luogu P1525 关押罪犯 练习一下)

但是这样只支持加边,回溯的时候删边怎么维护呢?考虑使用一个栈记录下对于并查集的所有操作,在回溯的时候反向操作就行了,但是这样也会出现一个问题——如果使用路径压缩那父子关系就会改变,回溯时就会导致复杂度爆炸,所以不能用路径压缩,那就用按秩合并,把深度浅的往深度深的合并,这样就行了

下面来分析一下复杂度

建树时一共有 \(m\) 条边,复杂度为 \(\Theta(m\log k)\),询问时需要遍历线段树上的每个节点,复杂度 \(\Theta(k)\),每到一个节点会操作一次并查集,复杂度 \(\Theta(\log n)\)

总时间复杂度 \(\Theta((m+k)\log n\log k)\)\(\Theta(m\log^2n)\)

Code

#include<bits/stdc++.h>  
#define in read()  
typedef long long ll;  
using namespace std;  
#define getchar() (S==T&&(T=(S=B)+fread(B,1,1<<18,stdin),S==T)?EOF:*S++)  
char B[1<<18],*S=B,*T=B;  
inline int read()  
{  
    char c=getchar();  
    int x=0;  
    while(c<48)c=getchar();  
    while(c>47)x=(x*10)+(c^48),c=getchar();  
    return x;  
}
//上面是读入优化

const int MAXM=2e5+5;  
int n,m,k;  
int fa[MAXM],d[MAXM];//并查集
pair<int,int>line[MAXM]; 
struct Node//线段树节点
{  
    int l,r;  
    vector<int>line;  
    Node()=default;  
}node[MAXM<<1];  
struct oper//对于并查集的操作
{  
    int x;  
    bool iseq;  
    oper()=default;  
    oper(int X,bool Iseq):x(X),iseq(Iseq){}  
};  
stack<oper>rec;//记录并查集操作的栈
inline void build(int pos,int l,int r)//线段树建树
{  
    node[pos].l=l,node[pos].r=r;  
    if(l==r) return;  
    int mid=(l+r)>>1;  
    build(pos<<1,l,mid);  
    build(pos<<1|1,mid+1,r);  
}  
inline void insert(int pos,int l,int r,int v)//线段树插入
{  
    if(node[pos].l>=l&&node[pos].r<=r) return node[pos].line.push_back(v),void(0);  
    int mid=(node[pos].l+node[pos].r)>>1;  
    if(l<=mid)insert(pos<<1,l,r,v);  
    if(r>mid)insert(pos<<1|1,l,r,v);  
}
inline int findf(int x){while(x!=fa[x])x=fa[x];return x;}//并查集找爸爸
inline void merge(int x,int y)//并查集合并
{  
    int fx=findf(x),fy=findf(y);  
    if(d[fx]>d[fy]) swap(fx,fy);  
    rec.push(oper(fx,d[fx]==d[fy]));//记录操作  
    fa[fx]=fy;  
    if(d[fx]==d[fy]) ++d[fy];  
}  
inline void solve(int pos)//遍历线段树
{  
    int sz=rec.size();  
    bool isok=1;  
    for(auto id:node[pos].line)  
    {  
        if(findf(line[id].first)==findf(line[id].second))
        //在同一个并查集就不满足
        {  
            for(int i=node[pos].l;i<=node[pos].r;++i) puts("No");  
            isok=0;  
            break;  
        }
        //满足就与敌对节点连边
        merge(line[id].first,line[id].second+n);  
        merge(line[id].second,line[id].first+n);  
    }  
    if(isok)
    {  
        if(node[pos].l==node[pos].r) puts("Yes");//叶子节点都满足就说明这个时间段满足
        else solve(pos<<1),solve(pos<<1|1);//不是叶子节点就递归求解  
    }  
    while(rec.size()-sz)//回溯
    {  
        if(rec.top().iseq) --d[fa[rec.top().x]];  
        fa[rec.top().x]=rec.top().x;  
        rec.pop();  
    }  
}  
signed main()  
{  
    n=in,m=in,k=in;  
    build(1,1,k);  
    for(int i=1,l,r;i<=m;++i)  
    {  
        line[i].first=in,line[i].second=in,l=in,r=in;  
        if(l!=r)insert(1,l+1,r,i);  
    }  
    for(int i=1,n2=n<<1;i<=n2;++i) fa[i]=i,d[i]=1;  
    solve(1);  
    return 0;  
}

例子2

传送门Luogu P5227 [AHOI2013]连通图

一句话题意 :给定一个有 \(n\) 个点 \(m\) 条边的无向连通图和 \(k\) 个小集合,第 \(i\) 个小集合包含 \(c_i\) 条边,对于每个集合,你需要确定将集合中的边删掉后改图是否保持联通。集合间的询问相互独立

数据范围\(1~\leq~n,k~\leq~10^5\) , \(1~\leq~m~\leq~2~\times~10^5\) , \(1~\leq~c~\leq~4\)

题解

注意到每条边都有时候有效(未被删除)有时候失效(被删除),每次询问都是询问一个状态时的信息,于是考虑线段树分治

同样建立一棵下标为 \([1,k]\) 的线段树,每一个节点上挂一个 \(\text{vector}\),维护在这段时间区间上有哪些边是有效的,把有效边的编号放进去,每条边所在的时间区间最多会被分割为 \(\log k\) 块(具体方法就是给每一条边开一个 \(\text{vector}\) 记录有那些时候是被删除的,在线段树上在被删除时刻的补集插入这条边的信息就行)

对于询问同样使用按秩合并的并查集,并查集上每个节点通过记录字数大小 \(\text{size}_i\) 来判断整个图是否联通,只要任意一个节点的最远祖先的子树大小 \(\text{size}_i=n\) 就能保证整个图联通

仍然用一个 \(\text{vector}\) 来记录并查集操作,回溯时反向操作回去就行了

关于复杂度,对线段树执行插入操作的次数规模是 \(kc\),插入复杂度为 \(\Theta(kc\log k)\),询问时需要遍历线段树上的每个节点,复杂度 \(\Theta(k)\),每到一个节点会操作一次并查集,复杂度 \(\Theta(\log n)\)

总时间复杂度 \(\Theta(kc\log n\log k)\)\(\Theta(kc\log^2n)\)

Code

#include<bits/stdc++.h>  
#define in read()  
using namespace std;  
#define getchar() (S==T&&(T=(S=B)+fread(B,1,1<<18,stdin),S==T)?EOF:*S++)  
char B[1<<18],*S=B,*T=B;  
inline int read()  
{  
    char c=getchar();  
    int x=0;  
    while(c<48)c=getchar();  
    while(c>47)x=(x*10)+(c^48),c=getchar();  
    return x;  
}  
const int MAXN=1e5+5,MAXM=2e5+5;  
int n,m,k;  
int fa[MAXN],d[MAXN],sz[MAXN];//并查集
pair<int,int>edge[MAXM];
vector<int>node[MAXN<<2],del[MAXM];//线段树节点,每条边删除的时刻  
struct oper//并查集操作
{  
    int x,y;  
    bool iseq;  
    oper()=default;  
    oper(int t1,int t2,int t3):x(t1),y(t2),iseq(t3){}  
};  
stack<oper>rec;//记录并查集操作的栈
inline void insert(int pos,int L,int R,int l,int r,int v)//线段树插入
{  
    if(l<=L&&R<=r) return node[pos].push_back(v),void(0);  
    int mid=(L+R)>>1;  
    if(l<=mid) insert(pos<<1,L,mid,l,r,v);  
    if(r>mid) insert(pos<<1|1,mid+1,R,l,r,v);  
}  
inline int findf(int x){while(x!=fa[x])x=fa[x];return x;}//并查集找爸爸
inline void merge(int x,int y)//并查集合并
{  
    x=findf(x),y=findf(y);  
    if(x==y) return;  
    if(d[x]>d[y]) swap(x,y);  
    fa[x]=y;  
    if(d[x]==d[y]) ++d[y];  
    sz[y]+=sz[x];  
    rec.push(oper(x,y,d[x]==d[y]));//记一下
}  
inline void solve(int pos,int L,int R)//遍历线段树
{  
    int top=rec.size();  
    for(auto id:node[pos])  
        merge(edge[id].first,edge[id].second);  
    if(L==R)//到叶子节点就判断满不满足
    {  
        if(sz[findf(1)]==n) puts("Connected");//任意节点最远祖先子树大小为n就满足了
        else puts("Disconnected");  
    }  
    else//没到就继续遍历
    {  
        int mid=(L+R)>>1;  
        if(L<=mid)solve(pos<<1,L,mid);  
        if(R>=mid)solve(pos<<1|1,mid+1,R);  
    }  
    while(rec.size()-top)//回撤并查集操作
    {  
        sz[rec.top().y]-=sz[rec.top().x];  
        if(rec.top().iseq) --d[rec.top().y];  
        fa[rec.top().x]=rec.top().x;  
        rec.pop();  
    }  
}  
signed main()  
{  
    n=in,m=in;  
    for(int i=1;i<=m;++i) edge[i].first=in,edge[i].second=in,del[i].push_back(0);  
    k=in;  
    for(int i=1,c;i<=k;++i)  
    {  
        c=in;  
        while(c--) del[in].push_back(i);  
    }  
    for(int i=1;i<=m;++i)  
    {  
        del[i].push_back(k+1);  
        for(int j=0,len=del[i].size()-1;j<len;++j)  
            if(del[i][j+1]-del[i][j]-1>0)  
                insert(1,1,k,del[i][j]+1,del[i][j+1]-1,i);  
    }  
    for(int i=1;i<=n;++i) fa[i]=i,d[i]=sz[i]=1;  
    solve(1,1,k);  
    return 0;  
}

习题推荐

Luogu P4585 火星商店问题

CodeForces 1140 F Extending Set of Points

CodeForces 576 E Painting Edges


博客园传送门

知乎传送门

posted @ 2022-08-01 15:32  人形魔芋  阅读(33)  评论(0编辑  收藏  举报