可能做到好題之後會再更新吧。
總敘
線段樹與離線詢問結合技巧又被稱為線段樹分治。
使用線段樹分治維護的資訊通常會在某一個時間段內出現,要求在離線的前提下回答某一個時刻的資訊並,則可以考慮使用線段樹分治的技巧。
以下是線段樹分治的基本模板:
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$,總時間為 $m$,所使用的資料結構滿足合併資訊的複雜度為 $O(M(n))$,使用棧撤銷的時間複雜度為 $O(T(n))$,在葉子結點統計答案的複雜度為 $O(C(n))$。
則每一個操作都線上段樹上被分割為 $O(\log{m})$ 段,總共有 $O(n\log{m})$ 段,每一段需要合併一次,刪除一次,總複雜度為 $O(n\log{m}(T(n)+M(n)))$。
加上統計答案則為 $O(n\log{m}(T(n)+M(n))+mC(n))$,通常,在題目中,我們限定 $n,m$ 同級。
二分圖 /【模板】線段樹分治
tips:擴充套件域並查集判二分圖屬於線段樹分治以外的知識點,請自行了解。
用線段樹分治解決問題需要思考以下問題:
- 線段樹維護什麼?
在這裡線段樹維護的是邊,透過邊出現的時間將其加入線段樹。對應模板中的 node 結構體。
- 使用什麼資料結構?
這裡是按秩合併的並查集,對應模板中的 type_name 結構體。
tips:為什麼不能使用路徑壓縮?
因為路徑壓縮是均攤複雜度,即 $T(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)$,$M(n)=O(\log{n})$,$C(n)=O(1)$,所以總的時間複雜度為 $O(m\log{k}(T(n)+M(n))+kC(n))=O(m\log{k}\log{n})$。
tips:本題的第 $i$ 個時間段代表的是開區間 $(i-1,i)$。
Fairy
*2900。
這道題也提示了線段樹分治的常見思想:控制邊的出現時間。
題目中要求去掉某一條邊,這個好辦,第 $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生成樹
要讓 $\operatorname{mex}=k$ 只要沒有權值為 $k$ 的邊不就行了。
要求去掉某一堆邊,這個好辦,讓權值為 $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$。
Communication Towers
*2700
看見一個點只在某段時間出現,果斷線段樹分治。
透過可撤銷並查集維護連通性。
這裡涉及到一個新問題,也是線段樹分治的一個trick:標記維護。
注意到需要給 $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_{imax}})$。
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$ 和只有一個點的集合 ${P}$。若集合 $S$ 的直徑為 $(U,V)$。則點集 $S∩{P}$ 的直徑只可能為 $(U,V)$,$(U,P)$ 或 $(V,P)$。
然後記錄直徑的兩端點做到撤銷,就可以線段樹分治了。
A Museum Robbery
*2800
一件展品出現有時間限制,很明顯的線段樹分治。
看 $s(m)$ 的計算方式,大概就是線段樹分治套揹包了。
令 $dp_i$ 為總重量為 $i$ 時的最大價值,問題就轉化為一個經典的 01 揹包問題了。統計答案時做一個字首和就可以了。
揹包撤銷時可以用退揹包,也可以 $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}+km)$。
Painting Edges
*3300
真史登場。
發現 $k\le50$ 然而空間不足以開 $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$)。
需要注意的是,無論修改後是否合法,屬於這個詢問的葉子結點上加入的這條邊一定是修改過後的顏色,因為你需要用修改過後的顏色去判斷它合不合法。
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)$ 級別。
請使用合理的方式或使用 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})$。