线段树分治总结(线段树分治,线段树,并查集,树的dfn序,二分图染色)

闲话

stO猫锟学长,满脑子神仙DS

网上有不少Dalao把线段树分治也归入CDQ分治?

还是听听YCB巨佬的介绍:

狭义:只计算左边对右边的贡献。
广义:只计算外部对内部的贡献。

看来可以理解为广义下的。

不过叫它线段树分治挺形象的啊!

线段树分治思想

我们在做CDQ的时候,将询问和操作通通视为元素,在归并过程中统计左边的操作对右边的询问的贡献。

而在线段树分治中,询问被固定了。按时间轴确定好询问的序列以后,我们还需要所有的操作都会影响一个时间区间。而这个区间,毫无疑问正好对应着询问的一段区间。

于是,我们可以将每一个操作丢到若干询问里做区间修改了,而线段树可以高效地维护。我们开一个叶子节点下标为询问排列的线段树,作为分治过程的底层结构。

具体的实现,仍然要看题目。

例题1

BZOJ4025二分图(点击进入题目

首先,图是二分图的充要条件是不存在奇环。

这个性质非常好。我们可以维护一棵生成树,假如当前要加入一条边\(x-y\),而生成树中\(x\)\(y\)的路径上有偶数条边,那么就会形成奇环。如果\(x\)\(y\)的路径上有奇数条边,就没问题,而且去掉这条边不会影响后续奇偶性的判断。这就让我们想到LCT做法:像WC2005双面棋盘一样,维护关于边删除时间的最大生成树,每次加边并判断。这里不详细展开。

关于维护奇偶性,我们还可以这样想:二分图染色!每次新加入一条边的时候,我们需要保证\(x,y\)之前其中之一未被染色或已染上不同颜色。因此我们参考NOIP2010关押罪犯,维护并查集,对每个点\(x\)额外建一个点\(x'\)表示它的反集,在加边时,如果\(x,y\)位于同一集合,将不再是二分图。否则合并\(x,y'\)\(y,x'\)

但是边有存在时间,而并查集显然不能随意删边。这时候,线段树分治的一大妙用就派上用场了。

发现每个时刻都要询问是否为二分图,我们就对时间建立线段树。对于每条边,我们就按照区间加法的模式,把边挂在出现时间对应线段树内的\(\log\)个节点上,用链表实现。分治时,从根节点出发,每到一个节点,将挂在该节点上的所有边合并,然后递归处理左儿子和右儿子。如果发现有某条边合并会出现奇环,那么显然可以断言,当前线段树节点所对应的时间区间都不会形成二分图。当成功到达叶子节点的时候,我们惊奇地发现,在这一时刻的所有边已经在并查集中,而这一时刻也得到了Yes的肯定答案!

一口气搞完了线段树分治流程的一大半,现在我们看看之前不能随意删边的问题。发现我们遍历线段树过程的本质,实际上是跑了一遍dfn序。这明显是一个栈的过程,不断向下遍历,同时加边;再进行回溯,我们要删去的是最后加的边!而使用按秩合并不路径压缩的并查集,我们就可以轻松做到可撤销最后一步的加边。实现的时候,也要写一个栈,保存每次并查集合并的有关信息(合并时加入的边,合并后树高\(dep\)的变化),在线段树中处理完左右子树后,将在当前节点加入的边删除。

分治过程至此结束。来分析一下复杂度。每条边会挂在\(\log T\)个线段树节点上。因为并查集没有路径压缩,所以每次加边删边判连通性时都需要跳\(\log n\)\(fa\)。于是得出了总复杂度\(m\log n\log T\)

写法上,蒟蒻还通过许多Dalao的博客了解到另一种用异或和维护奇偶性的并查集写法。比起蒟蒻提到的这种,常数小了一半,但感觉思路没那么简洁(其实是因为我这种弱鸡弄不懂比不上Dalao

实现细节方面,注意题目中时间段与时刻的区别(时间段从\(1\)开始,时刻从\(0\)开始)。数据中还有某些边出现了“瞬闪”的现象(出现时刻等于消失时刻),做区间加法时不特判一下会RE。数据中还有自环(不能构成二分图),不过使用蒟蒻这种并查集写法似乎没影响。

#include<cstdio>
#define I inline
#define RG register
#define R RG int
const int N=2e5,M=N<<1,L=4e6;
char buf[M],*pe=buf+M,*pp=pe-1;
int p,st[L],u[M],v[M],f[M],d[M],he[M],ne[L],id[L];
bool fl[L];
I void gc(){
    if(++pp==pe)fread(pp=buf,1,M,stdin);
}
I void pc(RG char C){
    if(++pp==pe)fwrite(pp=buf,1,M,stdout);*pp=C;
}
I int in(){
    gc();while(*pp<'-')gc();
    R x=*pp&15;gc();
    while(*pp>'-')x=x*10+(*pp&15),gc();
    return x;
}
I int get(R x){//直接跳fa
    while(f[x])x=f[x];
    return x;
}
I void merge(R x,R y){
    if(x==y)return;
    if(d[x]>d[y]){R t=x;x=y;y=t;}
    f[st[++p]=x]=y;//按秩合并,信息压入栈
    d[y]+=fl[p]=d[x]==d[y];//注意dep有没有变也要压
}
void upd(R t,R l,R r,R s,R e,R i){//区间加
    if(l==s&&r==e){
        ne[++p]=he[t];id[he[t]=p]=i;//挂链
        return;
    }
    R m=(l+r)>>1;
    if(e<=m)upd(t<<1,l,m,s,e,i);
    else if(s>m)upd(t<<1|1,m+1,r,s,e,i);
    else upd(t<<1,l,m,s,m,i),upd(t<<1|1,m+1,r,m+1,e,i);
}
void div(R t,R l,R r){
    R x,y,i,m=(l+r)>>1,lst=p;
    for(i=he[t];i;i=ne[i]){
        if((x=get(u[id[i]]))==(y=get(v[id[i]]))){
            for(;l<=r;++l)pc('N'),pc('o'),pc('\n');
            goto E;//出现奇环,断定整个区间不合法
        }
        merge(get(u[id[i]]+N),y);//合并反点
        merge(get(v[id[i]]+N),x);
    }
    if(l==r)pc('Y'),pc('e'),pc('s'),pc('\n');
    else div(t<<1,l,m),div(t<<1|1,m+1,r);
  E:for(;p>lst;--p)//撤销
        d[f[st[p]]]-=fl[p],f[st[p]]=0;
}
int main(){//g++:unused variable ‘n’。蒟蒻:叫我怎么用你?
    R n=in(),m=in(),t=in(),s,e;
    for(R i=1;i<=m;++i){
        u[i]=in();v[i]=in();s=in();e=in();
        if(s!=e)upd(1,1,t,s+1,e,i);//特判
    }
    pp=buf-1;div(1,1,t);
    fwrite(buf,1,pp-buf+1,stdout);
    return 0;
}

例题二

BZOJ3237[Ahoi2013]连通图(点击进入题目

这一个就没那么裸了,并没有体现任何询问的时间轴,以及修改对应的时间区间。

可是,一个图任意删边我们还是没法做,仍然要转化成加边。我们强行按照先后顺序对询问建时间轴,猛然发现,原来每条边的存在仍然是若干段时间区间!因为删边意味着由一段时间区间分裂成两段,所以总的段数控制在\(O(k)\)级别。

当然还是可以大力LCT。

当然接着使用线段树分治。这里的并查集使用带权的,维护好当前连通块大小。如果大小等于\(n\)说明图连通。

其它的部分代码几乎照搬。当然由删边的时间点转化为加边的时间区间需要写一个链表实现,蒟蒻使用了STL的list(又是第一次学一个STL。。。)

复杂度仍然是两个\(\log\),不过据说有随机权值的巧妙做法?

#include<cstdio>
#include<list>
#define I inline
#define RG register
#define R RG int
#define G c=getchar()
using namespace std;
const int N=2e5+9,M=4e5+9,L=8e6+9;
int n,p,st[M],f[N],s[N],d[N],a[M],b[M],he[M<<1],ne[L],id[L];
bool fl[M];
list<int>li[M];
I int in(){
    RG char G;
    while(c<'-')G;
    R x=c&15;G;
    while(c>'-')x*=10,x+=c&15,G;
    return x;
}
I int get(R x){
    while(f[x])x=f[x];
    return x;
}
void upd(R t,R l,R r,R s,R e,R i){
    if(l==s&&r==e){
        ne[++p]=he[t];id[he[t]=p]=i;
        return;
    }
    R m=(l+r)>>1;
    if(e<=m)upd(t<<1,l,m,s,e,i);
    else if(s>m)upd(t<<1|1,m+1,r,s,e,i);
    else upd(t<<1,l,m,s,m,i),upd(t<<1|1,m+1,r,m+1,e,i);
}
void div(R t,R l,R r){
    R i,x,y,m=(l+r)>>1,lst=p;
    for(i=he[t];i;i=ne[i]){
        if((x=get(a[id[i]]))==(y=get(b[id[i]])))continue;
        if(s[x]+s[y]==n){//剪枝,对复杂度并没有什么优化
            for(;l<=r;++l)puts("Connected");
            goto E;
        }
        if(d[x]>d[y])swap(x,y);
        s[f[st[++p]=x]=y]+=s[x];//带权合并
        d[y]+=fl[p]=d[x]==d[y];
    }
    if(l==r)puts("Disconnected");
    else div(t<<1,l,m),div(t<<1|1,m+1,r);
  E:for(;p!=lst;--p)
        d[f[x=st[p]]]-=fl[p],s[f[x]]-=s[x],f[x]=0;
}
int main(){
    n=in();R m=in(),k,i,lst;
    RG list<int>::iterator it;
    for(i=1;i<=n;++i)s[i]=1;
    for(i=1;i<=m;++i)
        a[i]=in(),b[i]=in();
    k=in();
    for(i=1;i<=k;++i)
        for(R c=in();c;--c)
            li[in()].push_back(i);
    for(i=1;i<=m;++i){
        li[i].sort();li[i].push_back(k+1);//排个序
        for(lst=1,it=li[i].begin();it!=li[i].end();++it){
            if(lst!=*it)upd(1,1,k,lst,*it-1,i);
            lst=*it+1;//把区间弄出来
        }
    }
    p=0;div(1,1,k);
    return 0;
}

转载于:https://www.cnblogs.com/flashhu/p/9404225.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是使用 C++ 实现使用并查集和生成找到无向图中的桥的代码: ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 1e5 + 5; int n, m, tot, cnt; int head[MAXN], dfn[MAXN], low[MAXN], fa[MAXN], id[MAXN], ans[MAXN]; bool vis[MAXN]; struct Edge { int to, nxt; } e[MAXN << 1], tree[MAXN << 1]; void addEdge(int u, int v) { e[++tot].to = v; e[tot].nxt = head[u]; head[u] = tot; } int find(int x) { if (x == fa[x]) { return x; } return fa[x] = find(fa[x]); } void merge(int x, int y) { int fx = find(x), fy = find(y); if (fx != fy) { fa[fx] = fy; } } void dfs(int u, int p) { dfn[u] = low[u] = ++tot; vis[u] = true; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].to; if (v == p) { continue; } if (!dfn[v]) { dfs(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) { ans[++cnt] = i; } else { merge(u, v); } } else if (vis[v]) { low[u] = min(low[u], dfn[v]); } } vis[u] = false; } void buildTree() { for (int i = 1; i <= m; i++) { int u = e[i].to, v = e[i ^ 1].to; int fu = find(u), fv = find(v); if (fu != fv) { tree[++tot].to = u; tree[tot].nxt = id[fv]; id[fv] = tot; tree[++tot].to = v; tree[tot].nxt = id[fu]; id[fu] = tot; fa[fu] = fv; } } } void dfs2(int u, int p) { dfn[u] = ++tot; for (int i = id[u]; i; i = tree[i].nxt) { int v = tree[i].to; if (v == p) { continue; } dfs2(v, u); } } int main() { scanf("%d %d", &n, &m); for (int i = 1; i <= n; i++) { fa[i] = i; } for (int i = 1; i <= m; i++) { int u, v; scanf("%d %d", &u, &v); addEdge(u, v); addEdge(v, u); } dfs(1, 0); tot = 0; buildTree(); tot = 0; dfs2(1, 0); printf("Bridges:\n"); for (int i = 1; i <= cnt; i++) { int u = e[ans[i]].to, v = e[ans[i] ^ 1].to; if (dfn[u] > dfn[v]) { swap(u, v); } printf("%d %d\n", u, v); } return 0; } ``` 在这个代码中,我们首先使用 DFS 遍历来查找桥。具体来说,我们记录了每个节点的 dfn 和 low 值,同时使用 vis 数组来判断一个节点是否已经被访问过。如果一个节点的 low 值大于其子节点的 dfn 值,则说明该节点与子节点之间的边是桥。否则,我们将该节点与子节点合并到同一个连通块中。 然后,我们使用并查集来构建生成,并使用 DFS 遍历生成来记录每个节点的 dfn 值。最后,我们遍历所有的桥,对于每一条桥 $(u, v)$,我们判断其在生成上的 dfs ,如果 $dfn_u < dfn_v$,则输出 $(u, v)$;否则,输出 $(v, u)$。 需要注意的是,这个算法的时间复杂度为 $O(m \log n)$,其中 $m$ 为边数,$n$ 为节点数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值