线段树分开治理
线段树分治:
夫线段树者,以二分之术,剖区间为段,层层递解。分治之道,在于将事析而治之,合而解之。线段树分治,乃取树形之构,先分后合,各治其域,终得全局之解。其法精妙,犹庖丁解牛,游刃有余。
线段树分治是一种处理动态问题离线化的高效算法,尤其适用于元素存在时间段明确且需支持撤销操作的场景。其核心思想是将时间轴映射到线段树结构上,通过分治策略降低复杂度。以下是关键要点:
一、算法组成
时间轴映射
将每个元素(如边、操作)的时间区间 \([L, R]\) 拆分为线段树上 \(O(logn)\) 个节点覆盖的区间,并将该元素存储到对应节点。
可撤销数据结构
需搭配支持回溯操作的数据结构(如可撤销并查集、线性基),通过栈记录操作步骤实现状态回退。关键要求:
1.禁用路径压缩:改用按秩合并(维护树高或大小)保证时间复杂度。
2.操作栈记录:合并集合时存储 (节点, 原父节点, 原秩) 三元组,回溯时逆向操作。
二、典型应用
动态图连通性与二分图判定
动态线性基维护
三、算法优势与局限
优势:
将动态问题转为静态离线处理
时间复杂度优化至 \(O(nlogn)\)
避免重复计算重叠子问题
局限:
仅支持离线查询
依赖可撤销数据结构
空间开销较大(需存储操作栈)
具体的,先看例题:
【模板】线段树分治
简要题意:
初始一张图( \(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树
具体的:
-
可持久化01Trie
- 用于高效处理异或最大值查询
- 每个版本对应一个时间点的商品集合
- 支持区间查询(类似可持久化线段树)
- 主要实现线段树分治对于时间段有关的维护
-
线段树分治
- 将关于时间的信息放 在线段树上进行操作
- 将普通商品的时间区间查询转换为在时间轴上的覆盖操作
- 对每个时间节点维护独立的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 x y c表示将 \((x,y)\) 这个点改为 \(c\).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\) 的证明 :转载

线段树分治
浙公网安备 33010602011771号