线段树分治

线段树分治

本身并不是很难理解的算法,实现起来难度也并不显著。


对于一些在某时间段有效的操作和对某一时间点的查询,可以离线后对时间轴建立一颗线段树,对于每个操作相当于在线段树上进行区间修改。

便利整颗线段树,到达每个节点执行相应的操作,向下递归至叶子节点,统计答案,回溯的时候撤销操作。、

在实现上,一个区间可能会进行多个操作,所以可以在线段树的每个节点上挂一个链表或 vector。由于线段树查询操作的性质,每个操作至多在线段树上加入 \(\log n\)vector,单次操作时间复杂度也是 \(O(\log n)\)。所以空间复杂度一般是 \(O(m\log n)\)\(m\) 为总操作次数。

最后递归时,一共进行 $m\log n $ 次操作,每次操作的时间复杂度为 \(k\)。那么总的时间复杂度为 \(O(km\log n)\)。在大多数题目中 \(k\)\(\log\) 级别的。

经典线段树分治问题

这些内容做法比较显然,具体来说,比较模板。

LG5787 二分图 /【模板】线段树分治

第一行三个整数 \(n,m,k\)

接下来 \(m\) 行,每行四个整数 \(x,y,l,r\),表示有一条连接 \(x,y\) 的边在 \(l\) 时刻出现 \(r\) 时刻消失。

询问在每个时刻这个图是否是二分图。

首先我们要会判定二分图。显然黑白染色一次的复杂度太高了,不能接受。可以考虑使用并查集

将一个点 \(u\) 拆成 \(u,u'\) 两个点,一个处于集合 \(S\) 中,一个处于集合 \(T\) 中。对于一条边 \((u,v)\),因为是个二分图,所以 \(u,v'\) 在集合 $S \(中,\)v,u'$ 在一个集合 \(T\) 中。如果出现了 \(u,u'\) 在一个集合中,也就说明了原图中存在了奇环,不是二分图。

因为为了满足撤销操作,需要使用可撤销并查集,为了满足时间复杂度,又得用按秩合并。如果你跟我一样不会写按秩合并,可以给每个节点一个随机值,随机值小的接在大的上,这样期望复杂度是正确的。

也没什么好讲的。看代码吧。

const int N=200005;
int n,m,k,fa[N<<1],siz[N<<1],stk[N<<1],top,key[N<<1];
inline int fidf(int x){
	while(x!=fa[x])x=fa[x];
	return x;
}
inline void merge(int x,int y){
	int fx=fidf(x),fy=fidf(y);
	if(key[fx]>key[fy])swap(fx,fy);
	siz[fy]+=siz[fx];fa[fx]=fy;
	stk[++top]=fx;
}
typedef pair<int,int>ttfa;
vector<ttfa>lis[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
void update(int p,int l,int r,int L,int R,ttfa v){
	if(L<=l&&r<=R){lis[p].push_back(v);return;}
	if(L<=mid)update(ls,l,mid,L,R,v);
	if(R>mid)update(rs,mid+1,r,L,R,v);
}
void solve(int p,int l,int r){
	int las=top;
	for(auto lin:lis[p]){
		if(fidf(lin.first)==fidf(lin.second)){
			for(int k=l;k<=r;++k)
				puts("No");
			goto end;
		}
		merge(lin.first,lin.second+n);
		merge(lin.second,lin.first+n);
	}
	if(l==r)puts("Yes");
	else{solve(ls,l,mid);solve(rs,mid+1,r);}
	end:
	while(top>las){
		int x=stk[top--];
		siz[fa[x]]-=siz[x];
		fa[x]=x;
	}
}
int main(){
	srand(time(0));
	n=read(),m=read(),k=read();
	for(int i=1;i<=m;++i){
		int a=read(),b=read(),l=read()+1,r=read()+1;
		if(l<r)update(1,1,k,l,r-1,{a,b});
	}
	for(int i=1;i<=n;++i){
		fa[i]=i,fa[i+n]=i+n;
		key[i]=rand(),key[i+n]=rand();
	}
	solve(1,1,n);
	return 0;
}//怎么还卡启发式合并啊。。。

LG5227 [AHOI2013]连通图

给定一个无向连通图和若干个小集合,每个小集合包含一些边,对于每个集合,你需要确定将集合中的边删掉后改图是否保持联通。集合间的询问相互独立

定义一个图为联通的当且仅当对于任意的两个顶点,都存在一条路径连接它们

首先如何动态判断一个图是否是连通图,还是可以使用并查集,只需要判断是否存在一个联通块包含所有的点,即这个联通块的大小是否为 \(n\) 即可。

至于题目给出的修改形式,似乎是单点的。但是我们可以化删除操作为加入操作,如果边 \(l\)\(t_1\) 时刻和 \(t_2\) 时刻被删除,我们可以认为它在 \([1,t_1-1],[t_1+1,t_2-1],[t_2+1,k]\) 的时刻被加入,多个时刻被删除也是同理,注意判断边界条件即可。

typedef pair<int,int>ttfa;
int n,m,k,las[N<<1],fa[N],key[N],stk[N<<1],top,siz[N];
inline int fidf(int x){while(x!=fa[x]){x=fa[x];}return x;}
inline bool merge(int x,int y){
	if(key[x]>key[y])swap(x,y);
	siz[y]+=siz[x];fa[x]=y;
	stk[++top]=x;return siz[y]==n;
}
vector<int>lis[N<<2];
ttfa lin[N];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
void update(int p,int l,int r,int L,int R,int id){
	if(L<=l&&r<=R){lis[p].push_back(id);return;}
	if(L<=mid)update(ls,l,mid,L,R,id);
	if(R>mid)update(rs,mid+1,r,L,R,id);
}
void solve(int p,int l,int r,bool flag){
	int las=top;
	for(auto i:lis[p]){
		int x=lin[i].first,y=lin[i].second,fx=fidf(x),fy=fidf(y);
		if(fx==fy)continue;
		if(merge(fx,fy))flag=1;
	}
	if(l==r){puts(flag?"Connected":"Disconnected");}
	else{solve(ls,l,mid,flag);solve(rs,mid+1,r,flag);}
	while(top>las){
		int x=stk[top--];
		siz[fa[x]]-=siz[x];
		fa[x]=x;
	}
}
int main(){
	srand(time(0));
	n=read(),m=read();
	for(int i=1;i<=n;++i)key[i]=rand(),siz[i]=1,fa[i]=i;
	for(int i=1;i<=m;++i)lin[i].first=read(),lin[i].second=read();
	k=read();
	for(int i=1;i<=k;++i){
		int l=read();
		for(int j=1;j<=l;++j){
			int x=read();
			if(las[x]+1<=i-1)
				update(1,1,k,las[x]+1,i-1,x);
			las[x]=i;
		}
	}
	for(int i=1;i<=m;++i)
		if(las[i]+1<=k)	
			update(1,1,k,las[i]+1,k,i);
	solve(1,1,k,0);
	return 0;
}

CF1140F Extending Set of Points

定义一个点集合 \(S=\{(x_i,y_i)\}(1\leq i\leq n)\) 的拓展操作为将符合以下条件的 \((x_0,y_0)\) 加入 \(S\)

  • 存在 \(a,b\),使得 \((a,b),(a,y_0),(x_0,b)\in S\)

不断执行以上操作直到不能操作,此时得到的集合即为拓展集合。现在给定 \(q\) 个操作,每次加入或删除一个点,重复点即为删除,你需要输出每个操作之后的拓展集合大小(不用真的拓展,只求大小)。

先思考题目给我们的要求怎么维护。

不放将 \((a,b)\) 这样一个二维点是做图上的一条边,\((a,y_0),(x_0,b)\) 同理,容易发现这其实是一张二分图,至于我们可以加边 \((x_0,y_0)\),也就是将这张二分图变成完全二分图。

我们完全可以把横坐标 \(a\) 看作二分图左侧的点,纵坐标 \(b\) 看作二分图右侧的点,当前二分图联通,则这个一定可以拓展出左右两个点集的完全二分图,答案就是左侧点集大小乘上右侧点集大小。

实现的时候并查集维护一个 \(siza\)\(sizb\),初始化的时候左侧点 \(siza=1\),右侧点 \(sizb=1\)。合并的时候两个 \(siz\) 分别相加,答案先减去两个联通块本身的答案,再加上合并后的答案。

撤销的时候操作相反。

至于如何将输入数据转化为区间操作,也可以见代码。

const int N=600005,M=300000;
int q,las[N],sta[N];
int stk[N<<1],top,fa[N<<1],key[N<<1];
ll now,siza[N<<1],sizb[N<<1];
inline int fidf(int x){
	while(x!=fa[x])x=fa[x];
	return x;
}
inline void merge(int x,int y){
	if(key[x]>key[y])swap(x,y);
	now-=siza[x]*sizb[x]+siza[y]*sizb[y];
	fa[x]=y;siza[y]+=siza[x],sizb[y]+=sizb[x];
	now+=siza[y]*sizb[y];
	stk[++top]=x;//没写这个,小丑了
}
typedef pair<int,int>ttfa;
map<ttfa,int>vis;int tot;
ttfa a[N],b[N];vector<ttfa>lis[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
void update(int p,int l,int r,int L,int R,ttfa v){
	if(L<=l&&r<=R){lis[p].push_back(v);return;}
	if(L<=mid)update(ls,l,mid,L,R,v);
	if(R>mid)update(rs,mid+1,r,L,R,v);
}
void solve(int p,int l,int r){
	int las=top;
	for(auto lin:lis[p]){
		int x=fidf(lin.first),y=fidf(lin.second+M);
		if(x==y)continue;
		merge(x,y);
	}
	if(l==r)printf("%lld ",now);
	else {solve(ls,l,mid);solve(rs,mid+1,r);}
	while(top>las){
		int x=stk[top--];
		now-=siza[fa[x]]*sizb[fa[x]];
		siza[fa[x]]-=siza[x],sizb[fa[x]]-=sizb[x];
		now+=siza[fa[x]]*sizb[fa[x]]+siza[x]*sizb[x];
		fa[x]=x;
	}
}
int main(){
	srand(14155);q=read();
	for(int i=1;i<=M;++i)fa[i]=i,siza[i]=1,key[i]=rand();
	for(int i=M+1;i<=M+M;++i)fa[i]=i,sizb[i]=1,key[i]=rand();
	for(int i=1;i<=q;++i){
		a[i].first=read(),a[i].second=read();
		if(vis.find(a[i])==vis.end()){
			vis[a[i]]=++tot;
			b[tot]=a[i];
		}
	}
	for(int i=1;i<=q;++i){
		int id=vis[a[i]];
		if(sta[id]==1&&las[id]<=i-1)
			update(1,1,q,las[id],i-1,a[i]);
		sta[id]^=1,las[id]=i;
	}
	for(int i=1;i<=tot;++i)
		if(sta[i]==1&&las[i]<=q)
			update(1,1,q,las[i],q,b[i]);
	solve(1,1,q);
	return 0;
}

有一定转化的问题

CF576E Painting Edges

给定一张 \(n\) 个点 \(m\) 条边的无向图。

一共有 \(k\) 种颜色,一开始,每条边都没有颜色。

定义合法状态为仅保留染成 \(k\) 种颜色中的任何一种颜色的边,图都是一张二分图。

\(q\) 次操作,第 \(i\) 次操作将第 \(e_i\) 条边的颜色染成 \(c_i\)

但并不是每次操作都会被执行,只有当执行后仍然合法,才会执行本次操作。

你需要判断每次操作是否会被执行。

\(n,m,q \le 5 \times 10^5\)\(k \le 50\)

此题加上了操作可能会无法执行的限制,似乎是无法离线了。

但是加入现在对边 \(u\)\(t_1\) 时刻和 \(t_2\) 时刻有两个染色操作,那么 \(t_1\) 时刻这个操作执行与否将影响 \([t_1+1,t_2-1]\) 这个操作是否执行。

如果执行了,那么就这条边可以染色,将这条边的实际修改的颜色标记一下,当前的答案为 YES,自然也可以实际就在 \([t_1+1,t_2-1]\) 的时间段上对并查集进行修改;如果无法执行,那么我们就将下次修改的颜色改成现在的合法的颜色即可。

这就要求我们对线段树加入操作的时候不要加入实际信息,而是加入一个编号即可。

至于判断二分图,和如何处理颜色(颜色很少),都是比较基础的问题,不多赘述。

const int N=500005,K=52;
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
typedef pair<int,int>ttfa;
ttfa line[N];
int n,m,k,p,las[N],idx[N],col[N],trv[N];
int fa[K][N<<1],key[N<<1],stk[N<<1],top,skc[N<<1];
inline int fidf(int c,int x){
	while(x!=fa[c][x])x=fa[c][x];
	return x;
}
inline void merge(int c,int x,int y){
	x=fidf(c,x),y=fidf(c,y);
	if(x==y)return;
	if(key[x]>key[y])swap(x,y);
	fa[c][x]=y;stk[++top]=x;skc[top]=c;
}

vector<int>lis[N<<2];
void update(int p,int l,int r,int L,int R,int id){
	if(L<=l&&r<=R){lis[p].push_back(id);return;}
	if(L<=mid)update(ls,l,mid,L,R,id);
	if(R>mid)update(rs,mid+1,r,L,R,id);
}
void solve(int p,int l,int r){
	int lst=top;
	for(auto i:lis[p]){
		int x=line[idx[i]].first,y=line[idx[i]].second,c=col[i];
		merge(c,x,y+n);merge(c,x+n,y);
	}
	if(l==r){//这个点的是没有参与更改的,因为这个点直接关乎后面的操作能否执行
		if(fidf(col[l],line[idx[l]].first)==fidf(col[l],line[idx[l]].second))
			{puts("NO");col[l]=trv[idx[l]];}//同时将这个区间修改的颜色改为现在的颜色
		else {puts("YES");trv[idx[l]]=col[l];}//更新现在的颜色
	}else{solve(ls,l,mid);solve(rs,mid+1,r);}
	while(top>lst){
		int x=stk[top],c=skc[top];--top;
		fa[c][x]=x;
	}
}
int main(){
	srand(1919810+114514);
	n=read(),m=read(),k=read(),p=read();
	for(int i=1;i<=k;++i){
		for(int j=1;j<=n;++j){
			fa[i][j]=j;
			fa[i][j+n]=j+n;
		}
	}
	for(int i=1;i<=n;++i)key[i]=rand();//the power of rand!
	for(int i=1;i<=m;++i){
		line[i].first=read(),line[i].second=read();
		las[i]=p+1;
	}
	for(int i=1;i<=p;++i)idx[i]=read(),col[i]=read();
	for(int i=p;i>=1;--i){
		if(i+1<las[idx[i]])
			update(1,1,p,i+1,las[idx[i]]-1,i);//因为一个如果在i位置修改无法成功,后续位置将无法修改,所以单独将后续位置提出来
		las[idx[i]]=i;
	}
	solve(1,1,p);
	return 0;
}

LG5631 最小mex生成树

给定 \(n\) 个点 \(m\) 条边的无向连通图,边有边权。

设一个自然数集合 \(S\)\(\text{mex}\) 为:最小的、没有出现在 \(S\) 中的自然数。

现在你要求出一个这个图的生成树,使得其边权集合的 \(\text{mex}\) 尽可能小。

这题并没有时间的要求,所以我们不能在形式上判定它是线段树分治的题目。但我们思考 \(\text{mex}\) 的本质,无法就是找到一个数,使得比它小的全都出现,而它本身没有出现。在思考生成树的限制,即现在一共有 \(n-1\) 条边,且如果是一个 kruskal 的过程,加边的时候一条边的两个端点原先不得联通。

我们会发现,如果一个点的边权为 \(w\),那么如果它在生成树中,\(\text{mex}\) 可以小于或大于 \(w\),但是不能等于 \(w\)

所以我们可以对 \(\text{mex}\) 的取值进行线段树分治(或者说对值域进行线段树分治)。如果存在边 \((u,v,w)\),那么在 \([0,w-1]\cup[w+1,\max w+1]\) 的取值区间内加上 \((u,v)\) 这条边 ,表明如果 \(\text{mex}\)\([0,w-1]\cup[w+1,\max w+1]\) 中的值,那么这条边可以加入生成树中。

然后我们便利生成树,找到一个合法的叶子节点,输出答案即可。

typedef pair<int,int>ttfa;
const int N=2000006,M=100005;
int n,m,maxw,uu[N],vv[N],ww[N];
int stk[N],top,res;
int fa[N],siz[N];
inline int fidf(int x){while(fa[x]!=x)x=fa[x];return x;}
inline void merge(int x,int y){
	if(siz[x]>siz[y])swap(x,y);
	siz[y]+=siz[x];fa[x]=y;
	--res;stk[++top]=x;
}
inline void split(int x){
	siz[fa[x]]-=siz[x];fa[x]=x;
	++res;
}
vector<ttfa>lis[M<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
void update(int p,int l,int r,int L,int R,ttfa v){
	if(L>r||R<l)return;//可能会因为R<0出错(有一条边权值为0)
	if(L<=l&&r<=R){lis[p].push_back(v);return;}
	if(L<=mid)update(ls,l,mid,L,R,v);
	if(R>mid)update(rs,mid+1,r,L,R,v);
}
void solve(int p,int l,int r){
	int now=top;
	for(auto lin:lis[p]){
		int fu=fidf(lin.first),fv=fidf(lin.second);
		if(fu!=fv)merge(fu,fv);
	}
	if(l==r){
		if(res==1){printf("%d\n",l);exit(0);}//线段树先左后右,满足答案最优
		while(top>now)split(stk[top--]);//wssb这里没有弹出,导致答案变小
		return;
	}
	solve(ls,l,mid);
	solve(rs,mid+1,r);
	while(top>now)split(stk[top--]);
}
int main(){
	res=n=read(),m=read();
	for(int i=1;i<=n;++i)fa[i]=i,siz[i]=1;
	for(int i=1;i<=m;++i){
		uu[i]=read(),vv[i]=read(),ww[i]=read();
		maxw=max(maxw,ww[i]);
	}
	++maxw;//是mex,可以取到最大的数+1
	for(int i=1;i<=m;++i){//wssb,这里之前循环写成1到n了。。
		update(1,0,maxw,0,ww[i]-1,{uu[i],vv[i]});
		update(1,0,maxw,ww[i]+1,maxw,{uu[i],vv[i]});
	}
	solve(1,0,maxw);
	return 0;
}

CF603E Pastoral Oddities

神仙题,建议看题解,这篇题解严格来说也是贺的。

给定一张 \(n\) 个点的无向图,初始没有边。

依次加入 \(m\) 条带权的边,每次加入后询问是否存在一个边集,满足每个点的度数均为奇数。

若存在,则还需要最小化边集中的最大边权。

\(n \le 10^5\)\(m \le 3 \times 10^5\)

还是和一般的线段树分治问题一样,我们要思考如何快速地判断题目给定的要求。然后就是一个神仙结论了:如果存在一个边集满足每个点的度数均为奇数,那么这个图中只有大小为偶数的联通块

证明:

必要性:加入存在奇数大小的联通块,由于每个点的度数都为奇数,所以度数之和也为奇数。然而每一条边都对总度数贡献度数为 \(2\),所以总度数必然为偶数,矛盾。

充分性:对于只有偶数个点的联通块,显然可以直接只留下点数除以 \(2\) 条边,满足每个点的度数为 \(1\)

然后考虑最小化边集中的最大边权这个限制了。解决这个问题只需按边权从小到大加边直到满足条件。

现在思考如何实现动态加边。(省略一些内容)如果一条边加入的时候没有进入边集中,那么再也不会进入。这启发我们一条边有一个影响范围,只有在这个范围内才会加入边集中。

考虑在线段树分治的过程中,每访问到一个叶子,我们都必然需要后移 Kruskal 的指针,将新的合法的边纳入答案直到合法为止。那么这个时间点就是这条边的影响范围的结束位置,而我们又知道每一条边的出现时间,于是事情就好起来了。

分治时应当从右到左递归。因为我们是要依次对每一个时间点求出答案,然后通过答案计算被最优边集包含的边影响区间。如果从左到右的话,得不到每一条边的影响区间的结束位置。

但是我们发现这是一个边分治边 cover 的处理过程,直接线段树分治起来可能会有点小问题,因为这个时间点上 cover 上的边不知道什么时候撤回掉。

这也很简单啊,只 cover 到当前时间减一就可以了,这个点上 cover 上的边在这个点直接撤回就完事了。(和上题是一样的)

这个算法的原理也很好理解:

我们每次在叶子节点找答案,祖先节点上 cover 上的时间戳必然合法,在这个点上 cover 上的边也会因为判断而只加起始时间在当前时间点之前的边。

原本我们的时间复杂度因为每一次暴力加边而变得不可接受。(复制粘贴嘤嘤嘤)

总的时间复杂度 \(O(m\log m \log n)\)

struct line{int u,v,x,id;}lin[N];int poi=1;
bool cmp(line x,line y){return x.x<y.x;}
int n,m,cnt,ans[N];//cnt统计大小为奇数的联通块的个数
int fa[N],siz[N],key[N],stk[N],top;
inline int fidf(int x){
	while(x!=fa[x])x=fa[x];
	return x;
}
inline void merge(int x,int y){
	x=fidf(x),y=fidf(y);
	if(x==y)return;
	if(key[x]>key[y])swap(x,y);
	if((siz[x]&1)&&(siz[y]&1))cnt-=2;
	siz[y]+=siz[x];fa[x]=y;stk[++top]=x;
}
vector<int>lis[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
void update(int p,int l,int r,int L,int R,int id){
	if(L<=l&&r<=R){lis[p].push_back(id);return;}
	if(L<=mid)update(ls,l,mid,L,R,id);
	if(R>mid)update(rs,mid+1,r,L,R,id);
}
void solve(int p,int l,int r){
	int las=top;
	for(auto i:lis[p])merge(lin[i].u,lin[i].v);
	if(l==r){
		while(cnt>0&&poi<=m){
			if(lin[poi].id<=l){
				merge(lin[poi].u,lin[poi].v);
				if(lin[poi].id<=l-1)
					update(1,1,m,lin[poi].id,l-1,poi);
			}//这还说明则条边对l以后的答案也不会产生贡献。(虽然仔细想想发现是废话)
			++poi;
		}
		if(!cnt)ans[l]=lin[poi-1].x;
		else ans[l]=-1;
	}else solve(rs,mid+1,r),solve(ls,l,mid);//先右后左
	while(top>las){
		int x=stk[top--];
		siz[fa[x]]-=siz[x];
		if((siz[fa[x]]&1)&&(siz[x]&1))cnt+=2;
		fa[x]=x;
	}
}
int main(){
	cnt=n=read(),m=read();
	for(int i=1;i<=n;++i)
		fa[i]=i,siz[i]=1,key[i]=rand();
	for(int i=1;i<=m;++i)
		lin[i].u=read(),lin[i].v=read(),lin[i].x=read(),lin[i].id=i;
	sort(lin+1,lin+1+m,cmp);
	solve(1,1,m);
	for(int i=1;i<=m;++i)
		printf("%d\n",ans[i]);
	return 0;
}

CF938G Shortest Path Queries

给出一个联通带权无向图,边有边权,要求支持 \(q\) 个操作

  • 1 x y d 在原图中加入一条 \(x\)\(y\) 权值为 \(b\) 的边。
  • 2 x y 把图中 \(x\)\(y\) 的边删掉
  • 3 x y 询问 \(x\)\(y\) 的异或最短路。

前置题目 LG4151 [WC2011]最大XOR和路径。

在上题中我们证明了路径上的点一定是 \(x\)\(y\) 的路径与简单换组成,而简单路径如何选择是无所谓的,只需要求出环的异或和并加入线性基中。

于是对于此题无加边删边,我们只需求出图的任意一颗生成树,然后求出非树边和树边形成的环并加入线性基中,在线性基中求异或最小值即可。

线性基的删除操作较难实现。有了加边删边之后,只需对时间进行线段树分治,将这条边加入对应的线段树上区间。容易发现递归的时候线性基最多只有线段树的深度个,进入一个新的深度的时候只需继承齐父亲节点的线性基,然后将在这个节点的操作所形成的环的异或和加入这个深度的线性基中。最后到根节点的时候求解最小异或和即可。

但是此题的图可能并不联通,此时需要使用可撤销并查集维护树的结构,由于异或的性质,我们只需要记录一个节点到该并查集根节点路径的异或和即可,利用这个信息即可求出环的异或和。

尚不成熟的线段树分治代码:

struct line{
	int u,v,w,fro,tar;
}e[N<<1];int cnt;
typedef pair<int,int>ttfa;
map<ttfa,int>vis;
vector<line>seg[N<<2];
void update(int p,int l,int r,line x){
	if(x.fro>x.tar)return;
	if(x.fro<=l&&r<=x.tar){
		seg[p].push_back(x);
		return;
	}
	if(l==r)return;
	int mid=(l+r)>>1;
	if(x.fro<=mid)update(p<<1,l,mid,x);
	if(x.tar>mid)update(p<<1|1,mid+1,r,x);
}
int n,m,q,fa[N],siz[N],val[N],stk[N],top;
int fidf(int x){while(fa[x]!=x){x=fa[x];}return x;}
int fidv(int x){int v=0;while(fa[x]!=x){v^=val[x];x=fa[x];}return v;}

int bas[75][33];
inline void insbas(int dep,int x){
	for(int i=30;i>=0;--i){
		if(!((x>>i)&1))continue;
		if(!bas[dep][i]){
			bas[dep][i]=x;
			for(int j=30;j>i;--j)
				if((bas[dep][j]>>i)&1)bas[dep][j]^=x;
			break;
		}else x^=bas[dep][i];
	}
}
void inser(int loc,int dep){
	for(int i=0;i<(int)seg[loc].size();++i){
		line tmp=seg[loc][i];
		int u=fidf(tmp.u),v=fidf(tmp.v),w=tmp.w,fro=tmp.u,tar=tmp.v;
		if(u!=v){
			if(siz[u]>siz[v])swap(u,v),swap(fro,tar);//启发式合并
			fa[u]=v;siz[v]+=siz[u];
			val[u]=fidv(fro)^fidv(tar)^w;
			stk[++top]=u;
		}else{
			w=fidv(fro)^fidv(tar)^w;
			insbas(dep,w);//有环,加入线性基
		}
	}
}
void goback(int lim){
	while(top>lim){
		int u=stk[top--];
		siz[fa[u]]-=siz[u];
		val[u]=0;
		fa[u]=u;
	}
}

struct quest{int u,v;}que[N];
void solve(int p,int l,int r,int dep){
	if(l>r)return;
	int tmp=top;inser(p,dep);
	if(l==r){
		int ans=fidv(que[l].u)^fidv(que[l].v);
		for(int i=30;i>=0;--i)
			if(bas[dep][i])ans=min(ans,ans^bas[dep][i]);
		printf("%d\n",ans);
		goback(tmp);
		return;
	}int mid=(l+r)>>1;
	for(int i=0;i<=30;++i)bas[dep+1][i]=bas[dep][i];
	solve(p<<1,l,mid,dep+1);
	for(int i=0;i<=30;++i)bas[dep+1][i]=bas[dep][i];
	solve(p<<1|1,mid+1,r,dep+1);
	goback(tmp);
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;++i)fa[i]=i,siz[i]=1;
	for(int i=1;i<=m;++i){
		int u=read(),v=read(),w=read();
		e[++cnt]=(line){u,v,w,1,-1};
		vis[make_pair(u,v)]=cnt;
	}
	q=read();int t=0;
	while(q--){
		int opt=read(),x=read(),y=read(),w;
		if(opt==1){
			w=read();
			e[++cnt]=(line){x,y,w,t+1,-1};
			vis[make_pair(x,y)]=cnt;
		}else if(opt==2){
			e[vis[make_pair(x,y)]].tar=t;
			vis[make_pair(x,y)]=0;
		}else que[++t]=(quest){x,y};
	}
	for(int i=1;i<=cnt;++i)
		if(e[i].tar==-1)e[i].tar=t;
	for(int i=1;i<=cnt;++i)
		update(1,1,t,e[i]);
	solve(1,1,t,0);
	return 0;
}

LG3733 [HAOI2017]八纵八横

上面这题的 1.5 倍经验。

保证了图联通,求异或最大值。同时值的二进制为 \(1000\),需要使用 bitset。所以还是要重新写的。

const int N=1004;typedef bitset<N> bit;
bit val[N];int mxl=0;
struct xorbas{
	bit bas[N];
	inline void insert(bit x){
		for(int i=mxl;i>=0;--i){
			if(!x[i])continue;
			if(bas[i].none()){bas[i]=x;break;}
			x^=bas[i];
		}
	}
	inline void pp(){
		bit x;
		for(int i=mxl;i>=0;--i)
			if(!x[i])x^=bas[i];
		bool flag=0;
		for(int i=mxl;i>=0;--i){
			if(x[i])flag=1;
			if(flag)putchar(x[i]+'0');
		}if(!flag)putchar('0');
		putchar('\n');
	}
};
int n,m,q,fa[N];
inline int fidf(int x){return x==fa[x]?x:fa[x]=fidf(fa[x]);}
inline bool merge(int x,int y){
	x=fidf(x),y=fidf(y);
	if(x==y)return 0;
	fa[x]=y;return 1;
}
struct option{int u,v,l,r;bit x;}opt[N<<1];int tot;
struct target{int v;bit x;};
vector<target>edge[N];
bit dis[N];
inline void addedge(int u,int v,bit w){
	edge[u].push_back((target){v,w});
	edge[v].push_back((target){u,w});
}
void dfs(int u,int f){
	for(auto tar:edge[u])
		if(tar.v!=f){dis[tar.v]=dis[u]^tar.x;dfs(tar.v,u);}
}
vector<int>lis[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
void update(int p,int l,int r,int L,int R,int id){
	if(L<=l&&r<=R){lis[p].push_back(id);return;}
	if(L<=mid)update(ls,l,mid,L,R,id);
	if(R>mid)update(rs,mid+1,r,L,R,id);
}
void dfstree(int p,int l,int r,xorbas tmp){
	for(auto it:lis[p])tmp.insert(dis[opt[it].u]^dis[opt[it].v]^opt[it].x);
	if(l==r){tmp.pp();return;}
	dfstree(ls,l,mid,tmp);dfstree(rs,mid+1,r,tmp);
}
int main(){
	cin>>n>>m>>q;tot=q;string s;
	for(int i=1;i<=n;++i)fa[i]=i;
	for(int i=1;i<=m;++i){
		int u,v;cin>>u>>v>>s;
		mxl=max(mxl,(int)s.size());
		if(merge(u,v))addedge(u,v,bit(s));
		else opt[++tot]=(option){u,v,0,q,bit(s)};
	}dfs(1,0);
	for(int i=1,idx=0;i<=q;++i){
		cin>>s;
		if(s=="Add"){
			int u,v;cin>>u>>v;cin>>s;
			mxl=max(mxl,(int)s.size());
			opt[++idx]=(option){u,v,i,q,bit(s)};
		}else if(s=="Cancel"){
			int tim;cin>>tim;
			opt[tim].r=i-1;
		}else{
			int tim;cin>>tim>>s;
			mxl=max(mxl,(int)s.size());
			opt[++tot]=opt[tim];opt[tot].r=i-1;
			opt[tim].l=i,opt[tim].x=bit(s);
		}
	}
	for(int i=1;i<=tot;++i){
		if(opt[i].u==0&&opt[i].v==0)continue;
		update(1,0,q,opt[i].l,opt[i].r,i);
	}xorbas bas;
	dfstree(1,0,q,bas);
	return 0;
}

非常规线段树分治

LG4585 [FJOI2015]火星商店问题

因为这题实在是太不寻常了,所以单独写了篇题解

posted @ 2022-08-15 15:41  BigSmall_En  阅读(82)  评论(0)    收藏  举报