Problems combining segment trees with offline queries also appear in the OI domain. This technique is also known as segment tree divide and conquer.
If you need to maintain some information that appears within a certain time interval, and you want to answer the union of information at a specific moment offline, you can consider using the segment tree divide and conquer technique.
In practice, segment tree divide and conquer is often used for the following purposes:
Simulating deletion operations using data structures that originally do not support deletion but support undo operations. For example, naive union-find cannot efficiently support edge deletion.
Computing data of different attributes separately. For example, when you need to find the answer for all colors except one.
If you don’t understand yet, don’t worry—we will illustrate both uses in example problems.
Process
First, we build a segment tree to maintain time moments. Each node maintains a vector to store information that belongs to that time interval.
Inserting information into the segment tree is similar to a regular segment tree range update.
Then we consider how to process the union of information for each time interval. We start divide and conquer from the root node, maintaining the current union of information. When reaching a node, we merge all the information at that node. When backtracking, we undo the contributions from this part. Finally, the union of information when reaching a leaf node is the corresponding answer.
If the time complexity of modifying information is \(O(T(n))\), we can set up a stack to record changes and undo them in \(O(T(n))\) time. The undo operation does not maintain amortized complexity.
The overall time complexity of the entire divide and conquer process is \(O(n\log n(T(n) + M(n)))\), where \(O(M(n))\) is the time complexity of merging information. The space complexity is \(O(n\log n)\).
#define ls (i << 1)#define rs (i << 1 | 1)#define mid ((l + r) >> 1)vector<Object>tree[N<<2];// Segment treevoidupdate(intql,intqr,Objectobj,inti,intl,intr){// Insertif(ql<=l&&r<=qr){tree[i].push_back(obj);return;}if(ql<=mid)update(ql,qr,obj,ls,l,mid);if(qr>mid)update(ql,qr,obj,rs,mid+1,r);}stack<Object>sta;// Stack for undoObjectnow;// Current union of informationObjectans[N];// Answersvoidsolve(inti,intl,intr){autolvl=sta.size();// Record the level to undo tofor(Objectx:tree[i])sta.push(now),now=Merge(now,x);// Merge informationif(l==r)ans[i]=now;// Record the answerelsesolve(ls,l,mid),solve(rs,mid+1,r);// Divide and conquerwhile(sta.size()!=lvl){// Undo informationnow=sta.top();sta.pop();}}
You need to maintain an undirected graph with \(n\) vertices and \(m\) edges. The \(i\)-th edge is \((x_i,y_i)\) and appears during the time interval \([l_i,r_i)\); it disappears at other times.
For each moment, if the graph is bipartite at that time, output Yes; otherwise, output No.
Solution
Use a union-find with categories to maintain whether the graph is bipartite, then apply segment tree divide and conquer.
Note: the undoable union-find cannot use path compression; it can only use union by rank.
#include<iostream>#include<stack>#include<vector>#define ls (i << 1)#define rs (i << 1 | 1)#define mid ((l + r) >> 1)usingnamespacestd;intn,m,k;constexprintN=2e5+5;intfa[N<<1],siz[N<<1];structUndoObject{intpos,val;UndoObject(intp,intv){pos=p,val=v;}};stack<UndoObject>undo_sz,undo_fa;intfind(intx){if(fa[x]==x)returnx;elsereturnfind(fa[x]);}voidmerge(intu,intv){intx=find(u),y=find(v);if(x==y)return;if(siz[x]<siz[y]){swap(x,y);}undo_sz.push(UndoObject(x,siz[x]));siz[x]+=siz[y];undo_fa.push(UndoObject(y,fa[y]));fa[y]=x;}voidundo(){fa[undo_fa.top().pos]=undo_fa.top().val;undo_fa.pop();siz[undo_sz.top().pos]=undo_sz.top().val;undo_sz.pop();}vector<int>tree[N<<2];voidupdate(intql,intqr,intv,inti,intl,intr){if(ql<=l&&r<=qr){tree[i].push_back(v);return;}if(ql<=mid)update(ql,qr,v,ls,l,mid);if(qr>mid)update(ql,qr,v,rs,mid+1,r);}structedge{intu,v;}g[N];vector<bool>ret;voidsolve(inti,intl,intr){autolevel=undo_fa.size();boolans=true;for(intu:tree[i]){inta=find(g[u].u);intb=find(g[u].v);if(a==b){for(intk=l;k<=r;k++){ret.push_back(false);}ans=false;break;}merge(g[u].u,g[u].v+n);merge(g[u].v,g[u].u+n);}if(ans){if(l==r){ret.push_back(true);}else{solve(ls,l,mid);solve(rs,mid+1,r);}}while(undo_fa.size()>level){undo();}}signedmain(){cin>>n>>m>>k;for(inti=1;i<=(n<<1);i++){fa[i]=i;siz[i]=1;}for(inti=1,l,r;i<=m;i++){cin>>g[i].u>>g[i].v>>l>>r;update(l+1,r,i,1,1,k);}solve(1,1,k);for(booli:ret){cout<<(i?"Yes":"No")<<'\n';}return0;}
Color Restriction
An undirected graph with \(n\) vertices and \(m\) edges, with \(k\) colors numbered \(0\sim k-1\). Each edge has one color.
For each color, determine whether the graph remains connected if all edges of that color are removed. Also determine whether it becomes a tree?
Output the number of colors for which the graph remains connected after removal, and the number of colors for which it becomes a tree.
Solution
For each color, create a time interval during which edges of that color are absent while all other edges are present. Maintain this using a union-find.
Maintain a forest of \(n\) vertices, initially isolated points.
There are \(q\) operations:
A x y Connect edge \((x,y)\).
Q x y Output the number of paths passing through edge \((x,y)\).
Offline is allowed.
Solution
Since offline is allowed, consider segment tree divide and conquer.
Then consider how to support Q operations. If edge \((x,y)\) does not exist, the answer is the product of the sizes of the connected components containing \(x\) and \(y\). This can be maintained with union-find.
Therefore, you can split each Q operation into three time points \(k-1,k,k+1\). Here \(k-1\) is the end time of the edge, and \(k+1\) is the start time of the edge. Thus at time \(k\) the edge is absent, which exactly answers the query.
Given a tree with \(n\) vertices, each vertex can be black or white. Initially all vertices are black. There are \(q\) operations:
C x Toggle the color of vertex \(x\).
G Query the maximum distance between two black vertices in the tree. If no black vertex exists, output \(-1\).
Offline is allowed.
Solution
First consider how to maintain the diameter of a set of points on a tree. There is the following lemma:
For a set \(S\) and a single-point set \(\{P\}\), if the diameter of \(S\) is \((U,V)\), then the diameter of \(S\cap\{P\}\) can only be \((U,V)\), \((U,P)\), or \((V,P)\).
Then consider solving the original problem. We can maintain the set of black vertices, recording for each vertex the time intervals during which it is in the black set (specifically, you can keep a bucket to record the last time it entered the black set).
Then naturally think of offline: insert all time points into a segment tree. Perform divide and conquer on the segment tree. Each node in the segment tree records the newly added vertices in the current time interval. For newly added points, use the lemma above to find the two endpoints of the diameter of the new set.
Undo is straightforward—just use a stack to record changes in the diameter endpoints.