CF276E Little Girl and Problem on Trees
此題為本人在Codeforces上獨立AC的第一道洛谷紫色難度的題目,極具紀念意義,特此釋出題解。
題意簡述
給定一棵無邊權的樹,初始點權均為0,保證除根節點外其餘節點的度數不超過2。執行m次操作,每次操作為以下兩種操作之一:(1)0 u x d 將距離節點u不超過d的點的權值加上x;(2)1 u 查詢節點u的權值。
演算法概述
顯然,樹的形態就是根上掛了很多條鏈,考慮利用這個形態的特殊性。
首先,如果距離d以內的點均與u在同一條鏈上,那麼很好處理,直接線段樹維護鏈上的區間權值即可,因為同一條鏈上的點dfs序是連續的,以下用id[u]表示節點u的dfs序。
但是這道題並沒有那麼簡單,這個d完全有可能超過dep[u],那麼就會繞過根影響到u所在的鏈以外的點,
如果直接將鏈以外的點的權值也一同處理了,則時間複雜度顯然不行。考慮在根上掛標記,將需要增加的點權暫時保留在根上。以val[i]表示距離根i以內的點需要加上的權值,這個定義等價於深度小於等於i的點需要加上的權值(定義根節點深度為0)。
我們用dep[u]表示節點u的深度,那麼在查詢節點u時,其保留在根上的還需增加的點權即為val[dep[u],dep[u]+1,…,n-1](一共n個節點,深度最大為n-1),也就是一段字尾和,故我們可以用樹狀陣列維護。
於是,我們就得到了一個可以很好解決這個問題的演算法:
對於修改操作:若d<dep[u],直接線段樹維護即可;若d>=dep[u],則先在根上加標記,val[d-dep[u]]加上x,然後處理鏈,由於在根上加了標記之後,原鏈上距離根d-dep[u]以內的點已經被覆蓋了,所以考慮有沒有過多覆蓋(即覆蓋的範圍超出了本來需要修改的範圍),這顯然不可能(反證:若過多覆蓋,即d-dep[u]>dep[u]+d => dep[u]<0,矛盾!)。所以已覆蓋的範圍是必然小於等於我們原本需要覆蓋的範圍的,那麼我們只需考慮還需修改的範圍(但是已覆蓋的範圍可能已經包括了鏈的末端,所以還需要特判)。這個範圍的頂端顯然是鏈頂(id[u]-dep[u]+1)加上已覆蓋的範圍(d-dep[u]),即id[u]-2*dep[u]+d+1,而末端可能為原鏈的鏈尾(id[u]+len[ch[u]]-dep[u]),也有可能是id[u]+d,兩者相比較一下取小即可。然後對於這個範圍,用線段樹做一下區間修改即可。
為了快速得到鏈尾,我們可以預處理出ch[u]表示節點u所在的鏈的編號,len[i]表示編號為i的鏈的長度,即鏈尾節點的深度。
對於查詢操作:一部分是已修改的權值,直接線段樹單點查詢即可,另一部分是待修改的權值,被我們掛在根上,故樹狀陣列區間查詢字尾和即可。
還有一個細節需要注意,由於樹狀陣列維護的下標範圍是從1開始的,而我們的深度最小是0,所以我們需要在對樹狀陣列執行操作時的下標整體後移一位。
參考程式碼
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=1e5+10; struct Edge{ int to,next; }edge[N<<1];int idx; int h[N]; void add_edge(int u,int v){edge[++idx]={v,h[u]};h[u]=idx;} struct Segment_Tree{ int l,r; ll sum; ll add; #define l(p) tree[p].l #define r(p) tree[p].r #define sum(p) tree[p].sum #define add(p) tree[p].add }tree[N<<2]; void push_up(int p){sum(p)=sum(p<<1)+sum(p<<1|1);} void spread(int p) { if(add(p)) { sum(p<<1)+=add(p)*(r(p<<1)-l(p<<1)+1); sum(p<<1|1)+=add(p)*(r(p<<1|1)-l(p<<1|1)+1); add(p<<1)+=add(p); add(p<<1|1)+=add(p); add(p)=0; } } void build(int p,int l,int r) { l(p)=l,r(p)=r; if(l==r)return; int mid=l+r>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); } void change(int p,int l,int r,int v) { if(l<=l(p)&&r>=r(p)) { sum(p)+=(ll)v*(r(p)-l(p)+1); add(p)+=v; return; } spread(p); int mid=l(p)+r(p)>>1; if(l<=mid)change(p<<1,l,r,v); if(r>mid)change(p<<1|1,l,r,v); push_up(p); } int query(int p,int pos) { if(l(p)==r(p))return sum(p); spread(p); int mid=l(p)+r(p)>>1; if(pos<=mid)return query(p<<1,pos); else return query(p<<1|1,pos); } int dep[N],id[N]; int len[N],ch[N]; int timestamp,cnt; void dfs(int p,int f,int ch_id) { id[p]=++timestamp; dep[p]=dep[f]+1; ch[p]=ch_id; //處理鏈的編號 len[ch_id]=max(len[ch_id],dep[p]); //每條鏈的最大深度 for(int i=h[p];~i;i=edge[i].next) { int to=edge[i].to; if(to==f)continue; dfs(to,p,ch_id); } } void init() { dep[1]=0; id[1]=++timestamp; for(int i=h[1];~i;i=edge[i].next) { int to=edge[i].to; dfs(to,1,++cnt); } } int tr[N]; int n,m; int lowbit(int x){return x&-x;} void add_tree(int x,int y){for(;x<=n;x+=lowbit(x))tr[x]+=y;} int ask(int x) { int ans=0; for(;x;x-=lowbit(x))ans+=tr[x]; return ans; } int main() { scanf("%d%d",&n,&m); memset(h,-1,sizeof h); for(int i=1;i<=n-1;i++) { int u,v;scanf("%d%d",&u,&v); add_edge(u,v); add_edge(v,u); } init(); build(1,1,n); while(m--) { int tp;scanf("%d",&tp); if(!tp) //修改操作 { int u,x,d;scanf("%d%d%d",&u,&x,&d); if(u==1)add_tree(d+1,x); //根節點直接特殊處理,d+1是下標後移一位 else if(d<dep[u]) //完全在鏈上的情況 change(1,max(id[u]-d,id[u]-dep[u]+1),min(id[u]+len[ch[u]]-dep[u],id[u]+d),x); //頂端取max,末端取min else //需要在根上掛標記的情況 { add_tree(d-dep[u]+1,x); //處理根上的標記,+1同樣也是因為下標後移一位 if(id[u]+len[ch[u]]-dep[u]<id[u]+d-2*dep[u]+1)continue; //鏈尾<未被覆蓋的第一個點,即鏈尾已被覆蓋 change(1,id[u]+d-2*dep[u]+1,min(id[u]+len[ch[u]]-dep[u],id[u]+d),x); //末端取min } } else //查詢操作 { int u;scanf("%d",&u); int ans=query(1,id[u]); //已經被修改的部分 int sum=ask(n)-ask(dep[u]); //還保留在根上的待修改部分 printf("%d\n",ans+sum); //兩部分相加即為答案 } } return 0; }