Loading

线段树分开治理

线段树分治:

夫线段树者,以二分之术,剖区间为段,层层递解。分治之道,在于将事析而治之,合而解之。线段树分治,乃取树形之构,先分后合,各治其域,终得全局之解。其法精妙,犹庖丁解牛,游刃有余。

线段树分治是一种处理动态问题离线化的高效算法,尤其适用于元素存在时间段明确且需支持撤销操作的场景。其核心思想是将时间轴映射到线段树结构上,通过分治策略降低复杂度。以下是关键要点:

一、算法组成

时间轴映射‌

将每个元素(如边、操作)的时间区间 \([L, R]\) 拆分为线段树上 \(O(log⁡n)\) 个节点覆盖的区间,并将该元素存储到对应节点。

可撤销数据结构‌

需搭配支持回溯操作的数据结构(如可撤销并查集、线性基),通过栈记录操作步骤实现状态回退。关键要求:

1.禁用路径压缩‌:改用按秩合并(维护树高或大小)保证时间复杂度‌。
2.操作栈记录‌:合并集合时存储 (节点, 原父节点, 原秩) 三元组,回溯时逆向操作‌。

二、典型应用

动态图连通性与二分图判定‌
动态线性基维护‌

三、算法优势与局限

优势‌:

将动态问题转为静态离线处理
时间复杂度优化至 \(O(nlog⁡n)\)
避免重复计算重叠子问题

局限:

仅支持离线查询
依赖可撤销数据结构
空间开销较大(需存储操作栈)

具体的,先看例题:

【模板】线段树分治

简要题意:

初始一张图( \(n\) 个节点),有 \(m\) 个操作,在 \(k\) 的时间总长里,有 \(m\) 条边会分别在 \(l_i\) 出现 \(r_i\) 消失,判断第 \(i\) 个时间段里这个图是否为二分图。

思路:

点击查看

因为这是线段树分治的模板题,所以我们考虑用线段树分治。

观察到本题需要我们判断是否为二分图且有关于时间的询问,而且我们对于删除这一操作难以处理,又把操作时间段放在了线段树上,遍历的时候可以认为是按时间遍历的,那么我们就需要一个支持撤销操作的判断二分图的算法,所以考虑扩展域并查集(不适用路径压缩)。

然后把时间段放在线段树上,然后就无了。

\(Code\)



#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int n,m,k,fa[N],val[N],tot;
struct edge{
    int x,y;
}e[N];//存边 
struct node{
    int x,y,val;
}st[N];
vector<int>q[N];
int find(int x) {while(x!=fa[x]) x=fa[x];return fa[x];}
void update(int u,int l,int r,int sl,int sr,int id) {
	if(sl<=l&&r<=sr) {
	    q[u].push_back(id);//存储覆盖这个时间点的边的编号 
	    return ;
	}
	int mid=l+r>>1;
	if(sl<=mid) update(u<<1,l,mid,sl,sr,id);
	if(sr>mid) update(u<<1|1,mid+1,r,sl,sr,id);
}
void merge(int x,int y) {
	int fx=find(x),fy=find(y);
	if(val[fx]>val[fy]) swap(fx,fy);
	st[++tot]=(node){fx,fy,val[fx]==val[fy]};
	fa[fx]=fy;
	if(val[fx]==val[fy]) val[fy]++;
}
void solve(int u,int l,int r) {
	int ans=1;
	int cnt=tot;
	for(auto v:q[u]) {
		int a=find(e[v].x);
		int b=find(e[v].y);
		if(a==b) {
			for(int k=l;k<=r;k++)
			printf("No\n");
			ans=0;
			break;
		}
		merge(e[v].x,e[v].y+n);
		merge(e[v].y,e[v].x+n);
	}
	if(ans) {
		if(l==r) printf("Yes\n");
		else {
			int mid=l+r>>1;
			solve(u<<1,l,mid);
			solve(u<<1|1,mid+1,r);
		}
	}
	//撤销之前的边 
	while(tot>cnt) {
		val[fa[st[tot].x]]-=st[tot].val;
		fa[st[tot].x]=st[tot].x;
		tot--;
	}
	return;
}
int main() {
    cin>>n>>m>>k;
	for(int i=1;i<=m;i++) {
		cin>>e[i].x>>e[i].y;
		int l,r;
		cin>>l>>r;l++;
		update(1,1,k,l,r,i);
	}
	for(int i=1;i<=n<<1;i++) fa[i]=i,val[i]=1;
	solve(1,1,k);
	return 0;
}
/*
3 3 3
1 2 0 2
2 3 0 3
1 3 1 2
*/




[AHOI2013] 连通图

简要题意:

给出一个无向连通图,若干次询问,每次询问问删除若干条边后图是否连通,询问独立。

思路:

点击查看

观察题面发现是关于联通性的询问,自然想到线段树分治,联通性用可撤销并查集维护。

但是他的修改操作是删边的,当然不如连边好维护,我们把删边操作变为加边操作,然后对于询问的时间轴分治


#include<bits/stdc++.h>
#define int long long
const int N=5e5+10;
using namespace std;
inline int read() {
	int x=0,f=1;char s=getchar();
	while(s<48||s>57)f=s=='-'?-1:1,s=getchar();
	while(s>=48&&s<=57)x=x*10+s-'0',s=getchar();
	return x*f;
}
int n,m,q,cnt,num;
stack< pair<int,int> >stk;
int size[N],fa[N],a[N],ans[N];
vector< int >edge[N];
pair<int,int> e[N];
int find(int x) {return x!=fa[x]?find(fa[x]):x;}
void merge(pair<int,int> s) {
	int x=find(s.first);
	int y=find(s.second);
	if(x==y) {
		stk.push(make_pair(-1,-1));
		return ;
	}
	num--;
	if(size[x]>size[y]) swap(x,y);
	fa[x]=y;
	size[y]+=size[x];
	stk.push(make_pair(x,y));
}
void del() {
	int x=stk.top().first;
	int y=stk.top().second;
	stk.pop();
	if(x==-1) return ;
	num++;
	fa[x]=x; size[y]-=size[x];
}
void update(int l,int r,int i,int x,int y,int s) {
	if(x>y) return ;
	if(x<=l&&r<=y) return edge[i].push_back(s),void();
	int mid=(l+r)>>1;
	if(x<=mid) update(l,mid,i<<1,x,y,s);
	if(y>mid) update(mid+1,r,i<<1|1,x,y,s);
}
void query(int l,int r,int i) {
	int sz=edge[i].size();
	for(auto v:edge[i]) merge(e[v]);
	if(l==r) {
		ans[l]=num==1?1:0;while(sz--)del();return ; 
	}
	int mid=(l+r)>>1;
	query(l,mid,i<<1); query(mid+1,r,i<<1|1);
	while(sz--) del();
}
signed main() {
	n=read(); m=read(); num=n;
	for(int i=1; i<=n; i++) fa[i]=i,size[i]=1;
	for(int i=1; i<=m; i++) {
		int x=read(),y=read();
		e[i]=make_pair(min(x,y),max(x,y));
		a[i]=1;
	}
	q=read();
	for(int i=1; i<=q; i++) {
		int k=read();
		while(k--) {
			int c=read();
			update(1,q,1,a[c],i-1,c);
			a[c]=i+1;
		}
	}	
	for(int i=1; i<=m; i++) update(1,q,1,a[i],q,i);
	query(1,q,1);
	for(int i=1; i<=q; i++) {
		if(ans[i]) printf("Connected\n");
		else printf("Disconnected\n");
	}
	return 0;
}





[FJOI2015] 火星商店问题

不简要但清楚的题意:

商店的编号 \(1 \sim n\) ,依次排列着 \(n\) 个商店。商店里出售的商品中,每种商品都用一个非负整数 \(\text{val}\) 来标价。每个商店每天都有可能进一些新商品,其标价可能与已有商品相同。

火星人购物时,会逛这条商业街某一段路上的商店,譬如说商店编号在区间 \([l,r]\) 中的商店,从中挑选一件自己最喜欢的商品。

每个火星人都有一个自己的喜好密码 \(x\)。他会购买 \(\text{val xor }x\) 的值最大的商品。

每个火星人只能购买最近 \(d\) 天内(含当天)进货的商品。另外,每个商店初始有一种商品,每个商店中每种商品都是无限的。

对于给定的按时间顺序排列的事件,计算每个购物的火星人的在本次购物活动中最喜欢的商品,即输出 \(\text{val xor }x\) 的最大值。这里所说的按时间顺序排列的事件是指以下两种事件:

0 s v,表示编号为 \(s\) 的商店在当日新进一种标价为 \(v\) 的商品。

1 l r x d,表示一位火星人当日在编号在 \([l,r]\) 的商店购买前 \(d\) 天内的商品,该火星人的喜好密码为 \(x\)

特别的:我们认为一个"0"操作算一天,及出现一个"0"就算新的一天。

思路:

点击查看

算法:线段树分治+可持久化字典树 或者 线段树套tire树

具体的:

  1. 可持久化01Trie

    • 用于高效处理异或最大值查询
    • 每个版本对应一个时间点的商品集合
    • 支持区间查询(类似可持久化线段树)
    • 主要实现线段树分治对于时间段有关的维护
  2. 线段树分治

    • 将关于时间的信息放 在线段树上进行操作
    • 将普通商品的时间区间查询转换为在时间轴上的覆盖操作
    • 对每个时间节点维护独立的Trie结构

时间复杂度 \(O(nlog^2n)\),线段树分治和trie树\(nlogn\),字典树查询\(logn\)

前缀知识:可持久化Trie树(推荐一篇写的很好的文章)

\(Code\):



#include<bits/stdc++.h>
#define mid ((l+r)>>1)
#define rc ((rt<<1)|1)
#define lc (rt<<1)
const int N = 1e5+10;
using namespace std;
inline int read() {
	int x=0,f=1; char s=getchar();
	while(s<48||s>57) f=s=='-'?-1:1,s=getchar();
	while(s>=48&&s<=57) x=x*10+s-'0',s=getchar();
	return x*f;
}
int n,m,cnt1,cnt2,tot,top;
int rt[N],ans[N],st[N];
int son[N*20][2],sz[N*20];
vector<int> a[N];//存储询问区间
struct edge {
	int l,r,tl,tr,x;
} p[N]; //p代表火星人  L,R 表示时间   l,r 表示他去的商店
struct node {
	int s,v,t;
} q[N],t1[N],t2[N]; //
bool cmp(node x,node y) {
	return x.s<y.s;
}
//可持久化Tire树
void insert(int &x,int u,int w) {
	int now;
	now=x=++tot;
	for(int i=17; i>=0; i--) {
		bool d=w&(1<<i);
		son[now][d^1]=son[u][d^1];
		son[now][d]=++tot;
		now=son[now][d];
		u=son[u][d];
		sz[now]=sz[u]+1;
	}
}
int query(int l,int r,int w) {
	int res=0;
	for(int i=17; i>=0; i--) {
		bool d=w&(1<<i);
		if(sz[son[r][d^1]]-sz[son[l][d^1]]>0)l=son[l][d^1],r=son[r][d^1],res+=(1<<i);
		else l=son[l][d],r=son[r][d];
	}
	return res;
}
void check(int x,int L,int R) {//计算在区间内哪个商品最优
	top=tot=0;
	for(int i=L; i<=R; i++) {
		st[++top]=q[i].s;
		insert(rt[top],rt[top-1],q[i].v);
	}
	for(int i=0,sz=a[x].size(); i<sz; i++) {
		int k=a[x][i],t;
		int l=upper_bound(st+1,st+1+top,p[k].l-1)-st-1;
		int r=upper_bound(st+1,st+1+top,p[k].r)-st-1;
		ans[k]=max(ans[k],t=query(rt[l],rt[r],p[k].x));
	}
}
void update(int rt,int l,int r,int L,int R,int x) {
	if(L>R||r<L||l>R)return ;
	if(L<=l&&r<=R) {
		a[rt].push_back(x);    //a 对于每个节点存储会来的火星入
		return;
	}
	update(lc,l,mid,L,R,x);
	update(rc,mid+1,r,L,R,x);
}
void go_work(int rt,int l,int r,int L,int R) {//按时间分治
	if(L>R)return;
	int cn1=0,cn2=0;
	check(rt,L,R);
	if(l==r)return;
	for(int i=L; i<=R; i++) //修改的区间右端点都是cnt1,相当于影响到之后的时间
		if(q[i].t<=mid)t1[++cn1]=q[i];
		else t2[++cn2]=q[i];
	for(int i=1; i<=cn1; i++) q[i+L-1]=t1[i]; //左端点在mid左边的放在左区间
	for(int i=1; i<=cn2; i++) q[i+L-1+cn1]=t2[i]; //否则放右边
	go_work(lc,l,mid,L,L+cn1-1);
	go_work(rc,mid+1,r,L+cn1,R);
}
int main() {
	n=read(),m=read();
	for(int i=1,x; i<=n; i++) insert(rt[i],rt[i-1],read());
	for(int i=1,opt,l,r,x,d,s,v; i<=m; i++) {
		opt=read();
		if(!opt) q[++cnt1]=(node) {read(),read(),cnt1};//cnt1指当前的时间
		else {
			l=read(),r=read(),x=read(),d=read();
			ans[++cnt2]=query(rt[l-1],rt[r],x);
			p[cnt2]=(edge) {
				l,r,max(1,cnt1-d+1),cnt1,x
			};
			//商店左端点,商店右端点,开始时间,结束时间,喜好密码
		}
	}
	for(int i=1; i<=cnt2; i++)update(1,1,cnt1,p[i].tl,p[i].tr,i); //把每个火星人按出现的编号放在线段树上
	sort(q+1,q+1+cnt1,cmp);//按照商店编号排序因为Tire树计算时需要与特殊商品一一对应 dd
	go_work(1,1,cnt1,1,cnt1);
	for(int i=1; i<=cnt2; i++)printf("%d\n",ans[i]);
	return 0;
}





[HAOI2017] 八纵八横

简要之题意:

初始给一个\(n\)个节点\(m\)条边的无向联通图(存在自环)每个点和边都有一个权值,将会有若干次操作,包括加边,删边,更改边的权值(操作对象不为初始的边),对于初始图和每个操作找到一个环,使得权值异或和最大(可经过重复的边和点)。

死路:

点击查看

做法:线性基+线段树分治

从 1 号节点出发再回到 1 号节点,能对答案产生贡献的只会是环。因为如果你走了一条链,你为了走回出发点只能原路返回或走另外一条路返回。如果原路返回它的每条边都经过了两次,异或起来对答案没有贡献;如果走另外一条路返回,那么这条路径本身就是一个环。

然后就是对于环的异或和的更新,考虑用线性基维护异或和。

每个时刻都做一次肯定是会 \(T\) 的。所以我们一开始的时候先将原图中已经存在的环丢进线性基,然后找一个原图的生成树。由于原图中的边是不会改变边权或者被删掉的。对于后面的每一次加边,我们把这条新边和原图的生成树所构成的那个环给丢进线性基里面。不太容易证明这些插入线性基中的环可以表示出来新图中的所有环。

自己的感性+理性证明:

我们对于题目中要求求出的环实际上只有两种有贡献:一种是从 \(1\) 的一条边出发,再从另一条边回来,构成一个大环,即 \(1\) 在环上,另一种大概是一个 $\rho $ 形的,是由一条链加上一个环构成的,易知链上的点是没有贡献的。

第一种就是我们加入到线性基里的环,另一种我们可以用两个环拼成,这样拼成的环也是从 \(1\) 的两条边,一条出去,另一条回来,是两条链和一个环,这两条链都走了两次,最终的贡献是一样的

然后就是边权更改和删边操作,容易发现更改边权可以看为删除原边,然后再加入一个新边,线性基维护删除是不易的,这就要用到线段树分治了,我们把操作放在时间轴上,然后把删边变对加边的撤销,即加边的逆运算,把每条边的时间区间 \([l,r]\) 求出来,然后插入线段树里面,然后递归到每一个节点的时候开一个新的线性基来备份当前状态。

空间复杂度:由于线段树高是 \(log Q\) 的,所以空间复杂度是 \(len logQ\)

注意到\(len\le1000\)所以用\(bitset\)存储权值。

时间复杂度比较复杂,简单来说 \(O(能过)\)

\(Code:\)



#include<bits/stdc++.h>
#define ls u<<1
#define rs u<<1|1
const int N=1e3+20;
using namespace std;
#define bitset bitset<1005>
struct Linear {
	bitset a[1005];
	bitset insert(bitset x) {
		for(int i=1000;~i;i--) {
			if(!x[i]) continue;
			if(!a[i].any())  return a[i]=x;
			else x^=a[i];
		}
		return x;
	}
	bitset query() {
		bitset sum;
		sum.reset();
		for(int i=1000;~i;i--) if(!sum[i]) sum^=a[i];
		return sum;
	}
	void print(bitset res) {
		bool f=0;
		for(int i=1000;~i;i--) {
			if(!f&&res[i]) f=1;
			if(f) putchar(res[i]+'0');
		}
		if(!f)putchar('0');
		putchar('\n');
	}
}pre;

struct union_find {
	int fa[N];
	void init() {for(int i=1;i<=1000;i++) fa[i]=i;}
	int find(int x) {if(fa[x]==x) return x;return fa[x]=find(fa[x]);}
	void merge(int x,int y) {x=find(x);y=find(y);fa[x]=y;}
}Pre;

struct node {int v,nxt;bitset w;} e[N<<1];
int head[N],cnt;
void add(int u,int v,bitset w) {e[++cnt].nxt=head[u];e[cnt].v=v;e[cnt].w=w;head[u]=cnt;}
bitset dis[N];

void dfs(int u,int fa) {
	for(int i=head[u];i;i=e[i].nxt) {
		int v=e[i].v;
		if(v==fa) continue;
		dis[v]=dis[u]^e[i].w;
		dfs(v,u);
	}
}

struct Edge{int u,v,l,r;bitset w;}pos[N<<1];
int n,m,Q,tot,mp[N],tmp;string opt,st;	
struct SGE {
	vector<int> a[N<<2];
	bitset ans[N];
	void update(int u,int l,int r,int x,int y,int k) {
		if(x<=l&&r<=y) return a[u].push_back(k),void();
		int mid=l+r>>1;
		if(x<=mid) update(ls,l,mid,x,y,k);
		if(y>mid) update(rs,mid+1,r,x,y,k);
	}
	void query(int u,int l,int r,Linear k) {
		for(auto v:a[u]) k.insert(dis[pos[v].u]^dis[pos[v].v]^pos[v].w);
		if(l==r) return ans[l]=k.query(),void();
		int mid=l+r>>1;
		query(ls,l,mid,k); query(rs,mid+1,r,k);
	}
} Tr;

int main() {
	scanf("%d%d%d",&n,&m,&Q);
	Pre.init();
	for(int i=1,x,y;i<=m;i++) {
		scanf("%d%d",&x,&y);
		cin>>st;
		if(Pre.find(x)!=Pre.find(y)) Pre.merge(x,y),add(x,y,bitset(st)),add(y,x,bitset(st));
		else pos[++tot]={x,y,0,Q,bitset(st)};
	}
	dfs(1,0);
	for(int i=1,x,y;i<=Q;i++) {
		cin>>opt;
		if(opt=="Add") {
			scanf("%d%d",&x,&y);
			cin>>st;
			pos[++tot]={x,y,i,Q,bitset(st)};
			mp[++tmp]=tot;
		}
		else if(opt=="Cancel") {
			scanf("%d",&x); pos[mp[x]].r=i-1;
		}
		else {
			scanf("%d",&x); cin>>st;
			pos[mp[x]].r=i-1;
			pos[++tot]=pos[mp[x]]; mp[x]=tot;
			pos[tot]={pos[tot].u,pos[tot].v,i,Q,bitset(st)};
		}
	}
	for(int i=1;i<=tot;i++) Tr.update(1,0,Q,pos[i].l,pos[i].r,i);
	Tr.query(1,0,Q,pre);
	pre.print(Tr.ans[0]);
	for(int i=1;i<=Q;i++) pre.print(Tr.ans[i]);
	return 0;
}


[湖北省选模拟 2024] 永恒 / eternity

简要题意:

初始给出一个 \(N*M\) 的图,每个点上有一个 # 或者是 \(0 \le x \le 9\) 的数字,还会有 \(Q\) 次修改操作,操作有两种:

  1. 1 x y c 表示将 \((x,y)\) 这个点改为 \(c\).
  2. 2 sx sy tx ty v 判断是否存在由 \((sx,sy)\) 出发,到达 \((tx,ty)\) 的移动方式,使得你积蓄了恰好为 \(v\) 的力量。保证 \((sx,sy)\)\((tx,ty)\) 不为 #.

规定不可以移动到为 # 的点移动方式为(上,下,左,右)四个方向.

积蓄力量:经过路径上的数字顺次连接最后 \(mod 114514\).例如,你经过的路径上点的数字依次为 \(3,1,0,3,3,3,2,1\),那么你积蓄的力量为 \(31033321 \bmod 1145141 = 114514\)

数据范围: \(N,M \le 500\), \(Q \le 2e5\)

思路:

点击查看

首先考虑极端情况 \((sx,sy) = (tx,ty)\) 并且被 # 包围, 判断 \(c_{i,j}\) 是否等于 \(v\) 即可

然后将这个图黑白染色,就有如下结论:

1.如果在 \((sx,sy)\)\((tx,ty)\) 的连通块中黑色点上的数不唯一,白色上的数也不唯一,那么就一定可以拼出 \(v\).

对于 \(1\) 的证明 :转载




posted @ 2025-06-06 15:13  dfgz  阅读(108)  评论(0)    收藏  举报