SP12005 GRASSPLA - Grass Planting 題解
阿新 • • 發佈:2022-04-20
Solution I
這幾乎是一道樹鏈剖分模板題,和模板題唯一的區別在於這題維護的是邊權。
因為除了根以外的節點都有父親,但是葉子數量很多,所以我們讓深度大的節點儲存邊的資訊,就方便處理很多了。
在操作的時候,因為深度大的節點儲存的才是邊的資訊,所以最頂端的節點是不能計算的。
如圖,若操作 \([2,5]\),那麼操作到最後(處於同一條重鏈上),就得操作頂端節點的重兒子。
#include <bits/stdc++.h> #define endl '\n' using namespace std; const int N = 1e5+10; int n,m; struct edge{ int to,nxt; }e[N<<1]; int head[N],idx; void add(int x,int y){ e[++idx]={y,head[x]}; head[x]=idx; } int fa[N],son[N],dep[N],siz[N],top[N]; int seg[N],dfn; void dfs1(int u,int f,int depth){ fa[u]=f;dep[u]=depth;siz[u]=1; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v!=f){ dfs1(v,u,depth+1); siz[u]+=siz[v]; if(siz[v]>siz[son[u]])son[u]=v; } } } void dfs2(int u,int tp){ top[u]=tp; seg[u]=++dfn; if(!son[u])return; dfs2(son[u],tp); for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(!seg[v]){ dfs2(v,v); } } } struct segment{ int l,r; int sum,add; }tr[N<<4]; #define ls(p) (p<<1) #define rs(p) (p<<1|1) inline void pushup(int p){tr[p].sum=tr[ls(p)].sum+tr[rs(p)].sum;} void build(int p,int l,int r){ tr[p].l=l;tr[p].r=r; if(l==r)return; int mid=l+r>>1; build(ls(p),l,mid); build(rs(p),mid+1,r); pushup(p); } void pushdown(int p){ if(tr[p].add){ tr[ls(p)].add+=tr[p].add; tr[rs(p)].add+=tr[p].add; tr[ls(p)].sum+=(tr[ls(p)].r-tr[ls(p)].l+1)*tr[p].add; tr[rs(p)].sum+=(tr[rs(p)].r-tr[rs(p)].l+1)*tr[p].add; tr[p].add=0; } } void tr_add(int p,int l,int r,int k){ if(l<=tr[p].l&&tr[p].r<=r){ tr[p].add+=k;tr[p].sum+=(tr[p].r-tr[p].l+1)*k; return; } pushdown(p); int mid=tr[p].l+tr[p].r>>1; if(l<=mid)tr_add(ls(p),l,r,k); if(mid<r)tr_add(rs(p),l,r,k); pushup(p); } int tr_query(int p,int l,int r){ if(l<=tr[p].l&&tr[p].r<=r)return tr[p].sum; pushdown(p); int mid=tr[p].l+tr[p].r>>1; int ans=0; if(l<=mid)ans+=tr_query(ls(p),l,r); if(mid<r)ans+=tr_query(rs(p),l,r); return ans; } void seg_add(int x,int y,int k){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); tr_add(1,seg[top[x]],seg[x],k); x=fa[top[x]]; } if(dep[x]>dep[y])swap(x,y); tr_add(1,seg[x]+1,seg[y],k); } int seg_query(int x,int y){ int ans=0; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); ans+=tr_query(1,seg[top[x]],seg[x]); x=fa[top[x]]; } if(dep[x]>dep[y])swap(x,y); ans+=tr_query(1,seg[x]+1,seg[y]); return ans; } int main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n>>m; for(int i=1,x,y;i<n;i++){ cin>>x>>y; add(x,y);add(y,x); } dfs1(1,0,1); dfs2(1,1); build(1,1,n); while(m--){ char op;int x,y; cin>>op>>x>>y; if(op=='P'){ seg_add(x,y,1); }if(op=='Q'){ cout<<seg_query(x,y)<<endl; } } return 0; }
Solution II
維護樹上問題還有一種更加強大的資料結構,就是 Link Cut Tree,簡稱 LCT。
當然這題用 LCT 來做簡直是降維打擊,LCT 的更多精彩操作請前往模板題。
跟樹鏈剖分一樣,LCT 通常維護點權,該如何維護邊權呢?
由於 LCT 是由動態的 splay 維護,父親及兒子會因為旋轉不停改變,所以不能像樹剖那樣儲存在深度大的節點。
解決方案是建虛節點,第 \(i\) 條邊的權值儲存在 \(i+n\) 個節點,連線 \(x\rightarrow y\) 這條邊就是連線 \(x\rightarrow i+n\),再連線 \(i+n\rightarrow y\)。至於 \(x\)
由於維護了邊權,pushup()
和 pushdown()
需要判斷 \(x\) 是邊(虛節點)還是普通節點。
#include <bits/stdc++.h> #define endl '\n' using namespace std; const int N = 1e6 + 10; int n,m; struct lct{ int son[2],fa,val,siz,sum; int add,rev; }tr[N<<2]; #define ls(x) (tr[x].son[0]) #define rs(x) (tr[x].son[1]) #define fa(x) (tr[x].fa) inline void pushup(int x){ tr[x].siz=tr[ls(x)].siz+tr[rs(x)].siz+x>n?1:0; tr[x].sum=tr[ls(x)].sum+tr[rs(x)].sum+tr[x].val; } inline bool isroot(int x){return !(ls(fa(x))==x || rs(fa(x))==x);} inline void reverse(int x){swap(ls(x),rs(x));tr[x].rev^=1;} inline void add(int x,int k){ if(x>n){ tr[x].val+=k;tr[x].sum+=tr[x].siz*k; } tr[x].add+=k; } void pushdown(int x){ if(tr[x].add){ if(ls(x))add(ls(x),tr[x].add); if(rs(x))add(rs(x),tr[x].add); tr[x].add=0; } if(tr[x].rev){ if(ls(x))reverse(ls(x)); if(rs(x))reverse(rs(x)); tr[x].rev=0; } } void pushall(int x){ if(!isroot(x))pushall(fa(x)); pushdown(x); } void rotate(int x){ int y=fa(x),z=fa(y); int k=rs(y)==x; if(!isroot(y))tr[z].son[rs(z)==y]=x; fa(x)=z; tr[y].son[k]=tr[x].son[k^1],fa(tr[x].son[k^1])=y; tr[x].son[k^1]=y,fa(y)=x; pushup(y);pushup(x); } void splay(int x){ pushall(x); while(!isroot(x)){ int y=fa(x),z=fa(y); if(!isroot(y)){ if((rs(z)==y) ^ (rs(y)==x))rotate(x); else rotate(y); } rotate(x); } } void access(int x){ for(int y=0;x;x=fa(y=x)){ splay(x);rs(x)=y;pushup(x); } } void makeroot(int x){ access(x);splay(x);reverse(x); } int findroot(int x){ access(x);splay(x); while(ls(x)){ pushdown(x); x=ls(x); } splay(x); return x; } void link(int x,int y){ makeroot(x); if(findroot(y)==x)return; fa(x)=y; } void split(int x,int y){ makeroot(x);access(y);splay(y); } int main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n>>m; for(int i=1,x,y;i<n;i++){ cin>>x>>y; tr[i+n].siz=1; link(x,i+n);link(i+n,y); } while(m--){ char p;int a,b; cin>>p>>a>>b; if(p=='P'){ split(a,b);add(b,1); }else{ split(a,b);cout<<tr[b].sum<<endl; } } return 0; }
時空對比
- 樹鏈剖分
- Link Cut Tree
可以發現,樹鏈剖分的時間複雜度是優於 LCT 的(線段樹快過 splay),但是在空間上以及碼量上,LCT 都勝過樹剖。