总叙
线段树与离线询问结合技巧又被称为线段树分治。
使用线段树分治维护的信息通常会在某一个时间段内出现,要求在离线的前提下回答某一个时刻的信息并,则可以考虑使用线段树分治的技巧。
以下是线段树分治的基本模板:
change 将信息按时间段覆盖在线段树上,query 通过不断合并线段树上节点维护的信息达到在叶子结点满足信息不重不漏。
struct type_name{};
struct del_tmp{};
struct node{};
struct segment_tree{
type_name name;
stack<del_tmp>st;
struct segment_tree_node{vector<node>v;}t[N<<2];
inline int ls(int x){return x<<1;};
inline int rs(int x){return x<<1|1;};
#define mid ((l+r)>>1)
void change(int p,int l,int r,int re_l,int re_r,node val){
if(re_l<=l&&r<=re_r) return t[p].v.push_back(val),void();
else{
if(re_l<=mid) change(ls(p),l,mid,re_l,re_r,val);
if(mid<re_r) change(rs(p),mid+1,r,re_l,re_r,val);
}
}
void query(int p,int l,int r){
int tp=st.tp;
for(auto it:t[p].v){
//do sonething to merge information
}
if(l==r) //do something to count answer
else query(ls(p),l,mid),query(rs(p),mid+1,r);
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
//do something to delete
}
}
}T;
时间复杂度分析
设总操作数为 n n n,总时间为 m m m,所使用的数据结构满足合并信息的复杂度为 O ( M ( n ) ) O(M(n)) O(M(n)),使用栈撤销的时间复杂度为 O ( T ( n ) ) O(T(n)) O(T(n)),在叶子结点统计答案的复杂度为 O ( C ( n ) ) O(C(n)) O(C(n))。
则每一个操作都在线段树上被分割为 O ( log m ) O(\log{m}) O(logm) 段,总共有 O ( n log m ) O(n\log{m}) O(nlogm) 段,每一段需要合并一次,删除一次,总复杂度为 O ( n log m ( T ( n ) + M ( n ) ) ) O(n\log{m}(T(n)+M(n))) O(nlogm(T(n)+M(n)))。
加上统计答案则为 O ( n log m ( T ( n ) + M ( n ) ) + m C ( n ) ) O(n\log{m}(T(n)+M(n))+mC(n)) O(nlogm(T(n)+M(n))+mC(n)),通常,在题目中,我们限定 n , m n,m n,m 同级。
二分图 /【模板】线段树分治
tips:扩展域并查集判二分图属于线段树分治以外的知识点,请自行了解。
用线段树分治解决问题需要思考以下问题:
- 线段树维护什么?
在这里线段树维护的是边,通过边出现的时间将其加入线段树。对应模板中的 node 结构体。
- 使用什么数据结构?
这里是按秩合并的并查集,对应模板中的 type_name 结构体。
tips:为什么不能使用路径压缩?
因为路径压缩是均摊复杂度,即 T ( n ) T(n) T(n) 可以达到 O ( n ) O(n) O(n)。会导致撤销的复杂度炸掉。
template<int N>struct DSU{
int fa[N],siz[N];
void clear(int n){for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;}
int find(int x){return (fa[x]==x?x:find(fa[x]));}
del_tmp merge(int x,int y){
int X=find(x),Y=find(y);
if(siz[X]>siz[Y]) swap(X,Y);
del_tmp ret={X,siz[X]};
siz[Y]+=siz[X],fa[X]=Y;
return ret;
}
bool same(int x,int y){return find(x)==find(y);}
};
3.怎么撤销操作?
通常是使用栈,撤销用模板中的 del_tmp 实现。
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
dsu.siz[dsu.fa[tmp.num]]-=tmp.siz;
dsu.fa[tmp.num]=tmp.num;
}
4.如何合并信息?
这道题就是使用并查集的 merge 函数完成,在 merge 过程中判断是否合法。
最后在叶子结点根据是否合法输出答案就行了。
code:
struct del_tmp{int num,siz;};
template<int N>struct DSU{
int fa[N],siz[N];
void clear(int n){for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;}
int find(int x){return (fa[x]==x?x:find(fa[x]));}
del_tmp merge(int x,int y){
int X=find(x),Y=find(y);
if(siz[X]>siz[Y]) swap(X,Y);
del_tmp ret={X,siz[X]};
siz[Y]+=siz[X],fa[X]=Y;
return ret;
}
bool same(int x,int y){return find(x)==find(y);}
};
struct edge{int u,v;};
struct segment_tree{
DSU<N<<1> dsu;
my_stack<del_tmp,N<<1>st;
segment_tree(){dsu.clear((N<<1)-1);}
struct segment_tree_node{vector<edge>v;}t[N<<2];
inline int ls(int x){return x<<1;};
inline int rs(int x){return x<<1|1;};
#define mid ((l+r)>>1)
void change(int p,int l,int r,int re_l,int re_r,edge val){
if(re_l<=l&&r<=re_r) return t[p].v.push_back(val),void();
else{
if(re_l<=mid) change(ls(p),l,mid,re_l,re_r,val);
if(mid<re_r) change(rs(p),mid+1,r,re_l,re_r,val);
}
}
void query(int p,int l,int r){
bool ok=1;int tp=st.tp;
for(auto it:t[p].v){
if(dsu.same(it.u,it.v)){
ok=0;
for(int i=l;i<=r;i++) cout<<"No\n";
break;
}//小优化,如果在这时已经不合法了,那么到了叶子结点只会增加边,仍然不合法。可以节约递归和删除成本
st.push(dsu.merge(it.u+N,it.v));
st.push(dsu.merge(it.v+N,it.u));
}
if(ok){
if(l==r) cout<<"Yes\n";
else query(ls(p),l,mid),query(rs(p),mid+1,r);//一定不能写反
}
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
dsu.siz[dsu.fa[tmp.num]]-=tmp.siz;
dsu.fa[tmp.num]=tmp.num;
}
}
}T;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m>>k;
for(int i=1,u,v,l,r;i<=m;i++){
cin>>u>>v>>l>>r;
if(l!=r) T.change(1,1,k,l+1,r,{u,v});//按时间加边
}
T.query(1,1,k);
}
在本题中, T ( n ) = O ( 1 ) T(n)=O(1) T(n)=O(1), M ( n ) = O ( log n ) M(n)=O(\log{n}) M(n)=O(logn), C ( n ) = O ( 1 ) C(n)=O(1) C(n)=O(1),所以总的时间复杂度为 O ( m log k ( T ( n ) + M ( n ) ) + k C ( n ) ) = O ( m log k log n ) O(m\log{k}(T(n)+M(n))+kC(n))=O(m\log{k}\log{n}) O(mlogk(T(n)+M(n))+kC(n))=O(mlogklogn)。
tips:本题的第 i i i 个时间段代表的是开区间 ( i − 1 , i ) (i-1,i) (i−1,i)。
Fairy
*2900。
这道题也提示了线段树分治的常见思想:控制边的出现时间。
题目中要求去掉某一条边,这个好办,第 i i i 条边在第 i i i 时刻不出现就可以了。
加边:
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
if(i!=1) T.change(1,1,m,1,i-1,{u,v});
if(i!=m) T.change(1,1,m,i+1,m,{u,v});
}
其余的部分也就大同小异了。
最小mex生成树
要让 mex = k \operatorname{mex}=k mex=k 只要没有权值为 k k k 的边不就行了。
要求去掉某一堆边,这个好办,让权值为 i i i 的边在第 i i i 时刻不出现就可以了。
在叶子结点判断剩余的边是否能构成原图的生成树即可。
query:
void query(int p,int l,int r){
int tp=st.tp;
for(auto it:t[p].v) st.push(dsu.merge(it.u,it.v));
if(dsu.siz[dsu.find(1)]==n) ans=min(ans,l);
else if(l!=r) query(ls(p),l,mid),query(rs(p),mid+1,r);
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
if(!tmp.num) continue;
dsu.siz[dsu.fa[tmp.num]]-=tmp.siz;
dsu.d[dsu.fa[tmp.num]]-=tmp.d;
dsu.fa[tmp.num]=tmp.num;
}
}
注意边权可能为 0 0 0。
Communication Towers
*2700
看见一个点只在某段时间出现,果断线段树分治。
通过可撤销并查集维护连通性。
这里涉及到一个新问题,也是线段树分治的一个trick:标记维护。
注意到需要给 1 1 1 所在的树上的每个结点打上标记,这并不好办。
考虑只给树的根结点增加标记。在撤销操作(其实也是分裂子树)时下传标记。
但这样会出现一种情况,一个子树后来连接上带有标记的根(但这个根现在不与 1 1 1 相连),则会导致新的子树也被统计进入答案。
考虑一个解决方法,在连接时减去根的标记,撤销时加上。
这样,只要根结点的标记在途中没有变化,就不会多下传。
//merge
del_tmp merge(int x,int y){
int X=find(x),Y=find(y);
if(X==Y) return{0,0};
if(d[X]>d[Y]) swap(X,Y);
del_tmp ret={X,d[X]==d[Y]};
d[Y]+=(d[X]==d[Y]),tag[X]-=tag[Y],fa[X]=Y;
return ret;
}
//delete
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
if(!tmp.num) continue;
dsu.tag[tmp.num]+=dsu.tag[dsu.fa[tmp.num]];
dsu.d[dsu.fa[tmp.num]]-=tmp.d;
dsu.fa[tmp.num]=tmp.num;
}
下传标记的问题至此完美解决,剩的就是普通线段树分治的操作了。
code
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,mx,l[N],r[N],ans=0x3f3f3f3f;
template<typename T,int siz> struct my_stack{
T st[siz];
int tp=0;
void push(T x){st[++tp]=x;}
void pop(){
if(!tp) cerr<<"THE STACK IS EMPTY!\n";
else tp--;
}
T top(){
if(!tp) cerr<<"THE STACK IS EMPTY!\n";
else return st[tp];
}
void clear(){tp=0;}
bool empty(){return !tp;}
};
struct del_tmp{int num,d;};
template<int N>struct DSU{
int fa[N],tag[N],d[N];
void clear(int n){for(int i=1;i<=n;i++) fa[i]=i,tag[i]=0,d[i]=1;}
int find(int x){return (fa[x]==x?x:find(fa[x]));}
del_tmp merge(int x,int y){
int X=find(x),Y=find(y);
if(X==Y) return{0,0};
if(d[X]>d[Y]) swap(X,Y);
del_tmp ret={X,d[X]==d[Y]};
d[Y]+=(d[X]==d[Y]),tag[X]-=tag[Y],fa[X]=Y;
return ret;
}
bool same(int x,int y){return find(x)==find(y);}
};
struct edge{int u,v;};
struct segment_tree{
DSU<N> dsu;
my_stack<del_tmp,N<<1>st;
segment_tree(){dsu.clear(N-1);}
struct segment_tree_node{vector<edge>v;}t[N<<2];
inline int ls(int x){return x<<1;};
inline int rs(int x){return x<<1|1;};
#define mid ((l+r)>>1)
void change(int p,int l,int r,int re_l,int re_r,edge val){
if(re_l<=l&&r<=re_r) return t[p].v.push_back(val),void();
else{
if(re_l<=mid) change(ls(p),l,mid,re_l,re_r,val);
if(mid<re_r) change(rs(p),mid+1,r,re_l,re_r,val);
}
}
void query(int p,int l,int r){
// cerr<<p<<" "<<l<<" "<<r<<"\n";
int tp=st.tp;
for(auto it:t[p].v) st.push(dsu.merge(it.u,it.v));
if(l==r) dsu.tag[dsu.find(1)]++;
else query(ls(p),l,mid),query(rs(p),mid+1,r);
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
if(!tmp.num) continue;
dsu.tag[tmp.num]+=dsu.tag[dsu.fa[tmp.num]];
dsu.d[dsu.fa[tmp.num]]-=tmp.d;
dsu.fa[tmp.num]=tmp.num;
}
}
}T;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>l[i]>>r[i],mx=max(mx,r[i]);
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
if(max(l[u],l[v])<=min(r[u],r[v])) T.change(1,1,mx,max(l[u],l[v]),min(r[u],r[v]),{u,v});
}
T.query(1,1,mx);
for(int i=1;i<=n;i++){
if(T.dsu.tag[i]) cout<<i<<" ";
}
}
时间复杂度 O ( m log n log R i m a x ) O(m\log{n}\log{R_{imax}}) O(mlognlogRimax)。
Unique Occurrences
*2300
将同颜色的边去掉后,每条此颜色的边两边的连通块大小之积即为这条边的贡献。
线段树分治+可撤销并查集即可。
洞穴勘测
将操作与查询一起放进线段树里分治。递归到叶子结点时如果是询问就回答,不是就不管。
离线统计边的存在时间,每条边依次插入线段树即可。
query:
void query(int p,int l,int r){
int tp=st.tp;
for(auto it:t[p].v) st.push(dsu.merge(it.u,it.v));
if(l==r){
if(q[l].u) cout<<(dsu.same(q[l].u,q[l].v)?"Yes\n":"No\n");//有改变的地方
}else query(ls(p),l,mid),query(rs(p),mid+1,r);
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
if(!tmp.num) continue;
dsu.d[dsu.fa[tmp.num]]-=tmp.d;
dsu.fa[tmp.num]=tmp.num;
}
}
add:
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>s>>u>>v;
if(u>v) swap(u,v);
if(s[0]=='C'){
int tmp=ma[{u,v}];
if(tmp) la[tmp]=i;
else ma[{u,v}]=++cnt,e[cnt]={u,v},la[cnt]=i;
}else if(s[0]=='D'){
int tmp=ma[{u,v}];
T.change(1,1,m,la[tmp],i-1,e[tmp]);
la[tmp]=0;
}else q[i]={u,v};
}
for(int i=1;i<=cnt;i++) if(la[i]) T.change(1,1,m,la[i],m,e[i]);
捉迷藏
问题变为了维护白点的直径,将每个点是白点的时间段插入线段树进行分治。当然你也可以用点分树。
直径的更新:
对于一个集合
S
S
S 和只有一个点的集合
P
{P}
P。若集合
S
S
S 的直径为
(
U
,
V
)
(U,V)
(U,V)。则点集
S
∩
P
S∩{P}
S∩P 的直径只可能为
(
U
,
V
)
(U,V)
(U,V),
(
U
,
P
)
(U,P)
(U,P) 或
(
V
,
P
)
(V,P)
(V,P)。
然后记录直径的两端点做到撤销,就可以线段树分治了。
A Museum Robbery
*2800
一件展品出现有时间限制,很明显的线段树分治。
看 s ( m ) s(m) s(m) 的计算方式,大概就是线段树分治套背包了。
令 d p i dp_i dpi 为总重量为 i i i 时的最大价值,问题就转化为一个经典的 01 背包问题了。统计答案时做一个前缀和就可以了。
背包撤销时可以用退背包,也可以 O ( n ) O(n) O(n) 记录修改之前的背包状态。我这里用的是后者。
code
#include<bits/stdc++.h>
using namespace std;
const long long N=1.5e4+5,M=3e4+5,W=1e3+5,p=1e7+19,mod=1e9+7;
int n,m,k,cnt,la[N];
long long power[W];
bool q[M];
struct exhabit{int c,w;}a[N];
struct segment_tree{
int dp[W];
struct segment_tree_node{vector<exhabit>v;}t[M<<2];
inline int ls(int x){return x<<1;};
inline int rs(int x){return x<<1|1;};
#define mid ((l+r)>>1)
void change(int p,int l,int r,int re_l,int re_r,exhabit val){
if(re_l<=l&&r<=re_r) return t[p].v.push_back(val),void();
else{
if(re_l<=mid) change(ls(p),l,mid,re_l,re_r,val);
if(mid<re_r) change(rs(p),mid+1,r,re_l,re_r,val);
}
}
void query(int p,int l,int r){
vector<int> pre(k+1,0);
for(int i=1;i<=k;i++) pre[i]=dp[i];
for(auto it:t[p].v){
for(int i=k;i>=it.w;i--) dp[i]=max(dp[i],dp[i-it.w]+it.c);
}
if(l==r){
if(q[l]){
long long ma=-1,ans=0;
for(int i=1;i<=k;i++){
ma=max(ma,1ll*dp[i]);
ans=(ans+ma*power[i-1]%mod)%mod;
}
cout<<ans<<"\n";
}
}else query(ls(p),l,mid),query(rs(p),mid+1,r);
for(int i=1;i<=k;i++) dp[i]=pre[i];
}
}T;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>k;cnt=n;
power[0]=1;
for(int i=1;i<=k;i++) power[i]=power[i-1]*p%mod;
for(int i=1;i<=n;i++) cin>>a[i].c>>a[i].w,la[i]=1;
cin>>m;
for(int i=1,opt,num;i<=m;i++){
cin>>opt;
if(opt==1){
cnt++;
cin>>a[cnt].c>>a[cnt].w;
la[cnt]=i;
}else if(opt==2){
cin>>num;
T.change(1,1,m,la[num],i,a[num]);
la[num]=0;
}else q[i]=1;
}
for(int i=1;i<=cnt;i++){
if(la[i]) T.change(1,1,m,la[i],m,a[i]);
}
T.query(1,1,m);
}
时间复杂度 O ( ( n + m ) k log m + k m ) O((n+m)k\log{m}+km) O((n+m)klogm+km)。
Painting Edges
*3300
真史登场。
发现 k ≤ 50 k\le50 k≤50 然而空间不足以开 50 50 50 棵线段树,那就只能是一个线段树里面 50 50 50 个并查集了。
先离线处理每条边颜色可能会更改的时间点(因为操作不合法时,颜色不会更改,所以不能直接插入),
然后线段树分治,在分治时分别添加各个颜色的边,如果有一个并查集不满足要求,整个子树都不合法。
回答完一个询问时,根据这个询问的信息获得边的颜色信息(即是否会更改),然后根据之前记录下的时间区间现场加到线段树后面去。
for(auto it:t[p].v){
if(dsu[it.c].same(it.u,it.v)){
ok=0;
for(int i=l;i<=r;i++){
cout<<"NO\n";
if(e[Q[i].num].c&&i+1<nxt[i]) change(1,1,q,i+1,nxt[i]-1,e[Q[i].num]);
}
break;
}
st.push(dsu[it.c].merge(it.u+N,it.v));st.st[st.tp].c=it.c;
st.push(dsu[it.c].merge(it.v+N,it.u));st.st[st.tp].c=it.c;
}
if(ok){
if(l==r){
cout<<"YES\n";
e[Q[l].num].c=Q[l].c;
if(l+1<nxt[l]) change(1,1,q,l+1,nxt[l]-1,e[Q[l].num]);
}else query(ls(p),l,mid),query(rs(p),mid+1,r);
}
其中 nxt 是下一次询问这条边的时间(如果没有下一次询问就是 q + 1 q+1 q+1)。
需要注意的是,无论修改后是否合法,属于这个询问的叶子结点上加入的这条边一定是修改过后的颜色,因为你需要用修改过后的颜色去判断它合不合法。
for(int i=1;i<=q;i++){
cin>>Q[i].num>>Q[i].c;
nxt[last[Q[i].num]]=i,last[Q[i].num]=i;//辅助记录修改时间区间
T.change(1,1,q,i,i,{e[Q[i].num].u,e[Q[i].num].v,Q[i].c});//强制加入修改后的颜色
}
代码实现部分完毕,但是这道题还卡空间。
tips1:如果你的做法里含有 STL 的 queue 并且数量为 O ( n ) O(n) O(n) 级别。
请使用合理的方式或使用 queue<int,list<int>>
更换掉,否则定义 queue 的额外内存会让你MLE。
tips2: 请计算好需要使用的空间,尽量不要多开任何无意义的空间。
code
#include<bits/stdc++.h>
using namespace std;
const int N=500005;
int n,m,q,k;
template<typename T,int siz> struct my_stack{
T st[siz];
int tp=0;
void push(T x){st[++tp]=x;}
void pop(){
if(!tp) cerr<<"THE STACK IS EMPTY!\n";
else tp--;
}
T top(){
if(!tp) cerr<<"THE STACK IS EMPTY!\n";
else return st[tp];
}
void clear(){tp=0;}
bool empty(){return !tp;}
};
struct del_tmp{int num,d;short int c;};
template<int N>struct DSU{
int fa[N],d[N];
void clear(int n){for(int i=1;i<=n;i++) fa[i]=i,d[i]=1;}
int find(int x){return (fa[x]==x?x:find(fa[x]));}
del_tmp merge(int x,int y){
int X=find(x),Y=find(y);
if(d[X]>d[Y]) swap(X,Y);
del_tmp ret={X,d[X]==d[Y],0};
d[Y]+=(d[X]==d[Y]),fa[X]=Y;
return ret;
}
bool same(int x,int y){return find(x)==find(y);}
};
struct edge{int u,v;short int c;}e[N];
struct query{int num;short int c;}Q[N];
int nxt[N],last[N];
struct segment_tree{
DSU<N<<1> dsu[51];
my_stack<del_tmp,N<<1>st;
void clear(int k){for(int i=1;i<=k;i++) dsu[i].clear((N<<1)-1);}
struct segment_tree_node{vector<edge>v;}t[N<<2];
inline int ls(int x){return x<<1;};
inline int rs(int x){return x<<1|1;};
#define mid ((l+r)>>1)
void change(int p,int l,int r,int re_l,int re_r,edge val){
if(re_l<=l&&r<=re_r) return t[p].v.push_back(val),void();
else{
if(re_l<=mid) change(ls(p),l,mid,re_l,re_r,val);
if(mid<re_r) change(rs(p),mid+1,r,re_l,re_r,val);
}
}
void query(int p,int l,int r){
bool ok=1;int tp=st.tp;
for(auto it:t[p].v){
if(dsu[it.c].same(it.u,it.v)){
ok=0;
for(int i=l;i<=r;i++){
cout<<"NO\n";
if(e[Q[i].num].c&&i+1<nxt[i]) change(1,1,q,i+1,nxt[i]-1,e[Q[i].num]);
}
break;
}
st.push(dsu[it.c].merge(it.u+N,it.v));st.st[st.tp].c=it.c;
st.push(dsu[it.c].merge(it.v+N,it.u));st.st[st.tp].c=it.c;
}
if(ok){
if(l==r){
cout<<"YES\n";
e[Q[l].num].c=Q[l].c;
if(l+1<nxt[l]) change(1,1,q,l+1,nxt[l]-1,e[Q[l].num]);
}else query(ls(p),l,mid),query(rs(p),mid+1,r);
}
while(st.tp>tp){
del_tmp tmp=st.top();st.pop();
dsu[tmp.c].d[dsu[tmp.c].fa[tmp.num]]-=tmp.d;
dsu[tmp.c].fa[tmp.num]=tmp.num;
}
}
}T;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m>>k>>q;
for(int i=1;i<=m;i++) cin>>e[i].u>>e[i].v;
for(int i=1;i<=q;i++) cin>>Q[i].num>>Q[i].c,nxt[last[Q[i].num]]=i,last[Q[i].num]=i,T.change(1,1,q,i,i,{e[Q[i].num].u,e[Q[i].num].v,Q[i].c});
for(int i=1;i<=m;i++) nxt[last[i]]=q+1;
T.clear(k);
T.query(1,1,q);
}
时间复杂度 O ( q log q log n ) O(q\log{q}\log{n}) O(qlogqlogn)。