【总结】线段树分治三题 -bzoj3237: [Ahoi2013]连通图&bzoj4025: 二分图&洛谷P3733 [HAOI2017]八纵八横


线段树分治

线段树分治解决一类离线问题,可将某一些操作的添加与删除按时间轴划分成几段加操作,而避免了不好实现的删除操作。

线段树分治的题目基本上都可以用cdq分治做,这两种做法本质上区别不大,只是线段树将cdq分治的操作提前安排到了对应的区间上,相对常数和内存要大一些。这篇讲的是线段树分治,就都按线段树方法写了。

线段树离线处理分段加的操作很模式化,关键在于如何在较低的复杂度内处理每个叶节点所代表时间的答案的统计。


bzoj3237 [Ahoi2013]连通图

判连通可以用启发式合并的并查集,记录并查集点上的 s i z e , d e p size,dep size,dep。只需判断最后节点1的祖先的 s i z e size size是否等于 n n n即可。

将每条边的出现时间拆成段, c ≤ 4 c\leq4 c4保证了这样的段不会很多,将这些段信息压入以时间轴为下标的线段树,每个节点上用 v e c t o r vector vector记入覆盖整个区间的边。

回答只需遍历线段树,维护一个栈,每次处理完当前节点回溯时,弹栈撤回之前并查集的合并操作。

#include<bits/stdc++.h>
#define RI register
#define gc getchar()
#define si isdigit(cp)
#define lc k<<1
#define rc k<<1|1
using namespace std;
const int N=2e5+100;

int n,m,pre[N],K,top;

char cp;
inline void rd(int &x)
{
	cp=gc;x=0;
	for(;!si;cp=gc);
	for(;si;cp=gc) x=x*10+(cp^48);
}

struct line{
  int u,v;
  inline void ini(){rd(u);rd(v);}
}le[N];

struct bcj{
    int fa,sz,dep;
    bcj(int fa_=0,int sz_=1,int dep_=1):fa(fa_),sz(sz_),dep(dep_){};
}b[N],res;

stack<bcj>S;

vector<int>t[N*22];

int F(int x){return x==b[x].fa?x:F(b[x].fa);}

inline void ins(int k,int l,int r,int L,int R,int pos)
{
	if(L<=l && r<=R) {t[k].push_back(pos);return;}
	RI int mid=(l+r)>>1;
	if(L<=mid) ins(lc,l,mid,L,R,pos);
	if(R>mid) ins(rc,mid+1,r,L,R,pos);
}

inline void sol(int k,int l,int r)
{
	RI int i,x,y,id,tp=top;
	for(i=t[k].size()-1;~i;--i){
		id=t[k][i];
		x=F(le[id].u);y=F(le[id].v);
		if(x==y) continue;
		top+=2;S.push(b[x]);S.push(b[y]);
		if(b[x].dep>b[y].dep) swap(x,y);
		b[x].fa=y;b[y].dep=max(b[y].dep,b[x].dep+1);b[y].sz+=b[x].sz;
	}
	if(l==r) puts(b[F(1)].sz==n?"Connected":"Disconnected");
	else{
	   RI int mid=(l+r)>>1;
	   sol(lc,l,mid);sol(rc,mid+1,r);	
	}
	for(;top>tp;S.pop(),--top) {res=S.top();b[res.fa]=res;} 
}

int main(){
	RI int i,j,x,y;
	rd(n);rd(m);
	for(i=1;i<=n;++i) b[i]=bcj(i,1,1);
	for(i=1;i<=m;++i) le[i].ini(),pre[i]=1;
	rd(K);
	for(i=1;i<=K;++i)
		for(rd(x);x;--x){
			rd(y);if(pre[y]<i) ins(1,1,K,pre[y],i-1,y);
			pre[y]=i+1;
		}
	for(i=1;i<=m;++i) if(pre[i]<=K) ins(1,1,K,pre[i],K,i);
	sol(1,1,K);
	return 0;
}

bzoj4025: 二分图

判断二分图只需要判断图中是否有奇环。

同样考虑启发式合并的并查集维护。出现奇环表现为:加入一条边时,两端点在同一并查集内且距离为偶数。

怎么用并查集判二分图呢?这里有一种很巧妙的方法:
记录每个点到其并查集上的父节点的距离的奇偶 d i s i dis_i disi。设 w i w_i wi表示 i i i i i i的祖先的距离的奇偶( d i s i = 1 / 0 , w i = 1 / 0 dis_i=1/0,w_i=1/0 disi=1/0,wi=1/0分别表示为奇/偶的情况),那么 w i w_i wi即为 i i i在跳 f a t h e r father father链时的 d i s i dis_i disi异或起来的值。

两个处于同一个并查集的点 x , y x,y x,y的距离奇偶即为 w x   x o r   w y w_x\ xor\ w_y wx xor wy

其他步骤就和上一道差不多了。

这道题得看样例解释:实际上左端点要+1。

#include<bits/stdc++.h>
#define RI register
#define gc getchar()
#define si isdigit(cp)
#define lc k<<1
#define rc k<<1|1
using namespace std;
const int N=1e5+100;

int n,m,T,ans[N],top,tot;

char cp;
inline void rd(int &x)
{
	cp=gc;x=0;
	for(;!si;cp=gc);
	for(;si;cp=gc) x=x*10+(cp-'0');
}

vector<int>hv[N<<2];

struct P{
  int u,v;
  inline void in(){rd(u);rd(v);}
}le[N<<1];

struct bcj{
  int fa,dep,dis;
  bcj(int fa_=0,int dep_=1,int dis_=0):fa(fa_),dep(dep_),dis(dis_){};
}b[N],bb;

stack<bcj>S;

int F(int x){
	for(;x!=b[x].fa;x=b[x].fa) tot^=b[x].dis;
	return x;
}

void ins(int k,int l,int r,int L,int R,int pos)
{
	if(L<=l && r<=R) {hv[k].push_back(pos);return;}
	RI int mid=(l+r)>>1;
	if(L<=mid) ins(lc,l,mid,L,R,pos);
	if(R>mid) ins(rc,mid+1,r,L,R,pos);
}

inline void sol(int k,int l,int r)
{
	RI int i,j,id,x,y,tp=top,ptr=1;
	for(i=hv[k].size()-1;~i;--i){
		id=hv[k][i];
		tot=0;x=F(le[id].u);y=F(le[id].v);
		if(x==y && tot==0){ptr=0;break;}
		if(b[x].dep>b[y].dep) swap(x,y);
		S.push(b[x]);S.push(b[y]);top+=2;
		b[x].fa=y;b[y].dep=max(b[y].dep,b[x].dep+1);
		b[x].dis=(tot^1);
	}
	if(ptr){
	  if(l==r) ans[l]=1;
	  else{
	  	RI int mid=(l+r)>>1;
	    sol(lc,l,mid);sol(rc,mid+1,r);
	  }
	}
	for(;top!=tp;S.pop(),--top) {bb=S.top();b[bb.fa]=bb;}
}

int main(){
	RI int i,j,u,v,l,r;
	rd(n);rd(m);rd(T);
	for(i=1;i<=m;++i){
		le[i].in();rd(l);rd(r);
		ins(1,1,T,l+1,r,i);
	}
	for(i=1;i<=n;++i) b[i]=bcj(i,1,0);
	sol(1,1,T);
	for(i=1;i<=T;++i) 
	 puts(ans[i]?"Yes":"No");
	return 0;
}

P3733 [HAOI2017]八纵八横

做过WC2011Xor的同学都知道,处理回路路径最大异或和可以将所有环异或和压入线性基,环边能异或出的最大值就是答案。

1000 × 1000 1000\times 1000 1000×1000的线性基显然压不进线段树节点,考虑只维护当前区间的线性基,随线段树下传修改。

那么如何快速 O ( 1 ) O(1) O(1)求出新加入的一条边形成的环的异或和呢?

题中说明了初始边不会删,且必然使图连通。所以可以构造一颗生成树,记录每个点到根之间的边的异或和 d i s i dis_i disi,每当加入一条额外的边权为 w w w的连接 x , y x,y x,y的边时,新形成的环的异或和即为 d i s x   x o r   d i s y   x o r   w dis_x\ xor \ dis_y \ xor\ w disx xor disy xor w

其他步骤就和上上道题差不多了。

#include<bits/stdc++.h>
#define gc getchar()
#define si isdigit(cp)
#define RI register
#define ws 999
#define lc k<<1
#define rc k<<1|1
using namespace std;
const int N=1010;
typedef bitset<N> bt;
bt w[N<<1],nw,dis[555],ans;
int n,m,q,pre[N],vis[555];
int head[555],to[N<<1],nxt[N<<1],tot;
char s[N<<1];

struct lb{bt v[N];}ori;
vector<bt>hv[N<<3];

char cp;
inline void rd(int &x)
{
    cp=gc;x=0;
    for(;!si;cp=gc);
    for(;si;cp=gc) x=x*10+(cp-'0');
}

inline void get(bt& x)
{
    RI int i,j;
    scanf("%s",s);
    j=strlen(s)-1;
    for(i=0;i<=j;++i) x[i]=s[j-i]-'0';
}

struct L{
  int u,v;bt val;
  inline void in(){rd(u);rd(v);}
}le[N<<1];

inline void lk(int u,int v,bt vv)
{to[++tot]=v;nxt[tot]=head[u];head[u]=tot;w[tot]=vv;}

inline void upp(lb &B,bt x)
{
    RI int i;
    for(i=ws;~i && x.any();--i)
     if(x[i]){
        if(!B.v[i].any()) {B.v[i]=x;break;}
        else x=x^B.v[i]; 
     }
}


inline void ask(lb B)
{
    RI int i,ptr=0;ans.reset();
    for(i=ws;~i;--i) 
     if(B.v[i].any()) break;
    if(i==-1) {puts("0");return;}
    for(ptr=i;~i;--i) 
      if(B.v[i].any() && !ans[i])
         ans=ans^B.v[i];
    for(i=ptr;~i;--i) putchar('0'+ans[i]);
    puts("");
}

void dfs(int x,int fa)
{
    RI int i,j;vis[x]=1;
    for(i=head[x];i;i=nxt[i]){
        j=to[i];if(j==fa) continue;
        if(vis[j]) upp(ori,dis[j]^dis[x]^w[i]);
        else{dis[j]=dis[x]^w[i];dfs(j,x);}
    }
}

void ins(int k,int l,int r,int L,int R,bt vv)
{
    if(L<=l && r<=R) {hv[k].push_back(vv);return;}
    RI int mid=(l+r)>>1;
    if(L<=mid) ins(lc,l,mid,L,R,vv);
    if(R>mid) ins(rc,mid+1,r,L,R,vv);
}

void sol(int k,int l,int r,lb ss)
{
    for(RI int i=hv[k].size()-1;~i;--i) 
      upp(ss,hv[k][i]);
    if(l==r) {ask(ss);return;}
    RI int mid=(l+r)>>1;
    sol(lc,l,mid,ss);sol(rc,mid+1,r,ss);
}

inline void init()
{
    RI int i,x,y,num=0;
    rd(n);rd(m);rd(q);
    for(i=1;i<=m;++i){
        rd(x);rd(y);
        nw.reset();get(nw);
        lk(x,y,nw);lk(y,x,nw);
    }
    dfs(1,0);
    for(i=1;i<=q;++i){
        scanf("%s",s);
        if(s[0]=='A'){
           pre[++num]=i;
           le[num].in();get(le[num].val);
        }else{
            rd(x);
            ins(1,1,q,pre[x],i-1,le[x].val^dis[le[x].u]^dis[le[x].v]);
            if(s[1]=='h'){
                pre[x]=i;le[x].val.reset();get(le[x].val);
            }else pre[x]=0;
        }
    }
    for(i=1;i<=num;++i) if(pre[i])
     ins(1,1,q,pre[i],q,le[i].val^dis[le[i].u]^dis[le[i].v]);
}

int main(){
    init();
    ask(ori);
    if(q) sol(1,1,q,ori);
    return 0;
}

没有考虑 q = 0 q=0 q=0的情况 R E RE RE了4个点…

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值