樹上差分利用前綴和的思想,利用樹上的前綴和(也就是子樹和),記錄樹上的一些信息,因為它可以進行離線操作,復雜度O(n),時間、空間、代碼復雜度都十分優秀。
最大流
FJ給他的牛棚的N(2≤N≤50,000)個隔間之間安裝了N-1根管道,隔間編號從1到N。所有隔間都被管道連通了。
FJ有K(1≤K≤100,000)條運輸牛奶的路線,第i條路線從隔間si運輸到隔間ti。一條運輸路線會給它的兩個端點處的隔間以及中間途徑的所有隔間帶來一個單位的運輸壓力,你需要計算壓力最大的隔間的壓力是多少。
這道題需要讓我們求出每個點的覆蓋次數(這顯然可以用樹鏈剖分來做),但這也是一個樹上差分的經典題。
拋開這道題,先考慮對於一維的一個空間,我們如何差分,沒錯,對於區間(l-r)來說,在l出加上1,在r+1處減去1,再求前綴和即可。
那在樹上如何操作,我們可以把樹抽象的想象成自下而上的一個數組,對於一段連續的鏈,在下面的端點加上1,在上面的端點的父節點減去1,求子樹和。
那么左右端點不在一條直鏈上怎么辦?可以考慮使用lCA,對於兩個點的LCA來說,LCA才這條鏈上只出現了一次,所以在兩個端點分別加1,LCA-1,LCA的父親減1,求子樹和,這道題就水過了。
#include<iostream> #include<cstdio> #define N 50009 using namespace std; int ans,head[N],ji[N],tot,deep[N],p[N][22],n,m,a,b,c,fa[N]; struct de { int n,to; }an[N<<1]; inline void add(int u,int v) { an[++tot].n=head[u]; an[tot].to=v; head[u]=tot; } void dfs(int u,int f) { deep[u]=deep[f]+1; fa[u]=f; p[u][0]=f; for(int i=1;(1<<i)<=deep[u];++i) p[u][i]=p[p[u][i-1]][i-1]; for(int i=head[u];i;i=an[i].n) if(an[i].to!=f) { int v=an[i].to; dfs(v,u); } } inline int getlca(int a,int b) { if(deep[a]<deep[b])swap(a,b); for(int i=20;i>=0;--i) if(deep[a]-(1<<i)>=deep[b])a=p[a][i]; if(a==b)return b; for(int i=20;i>=0;--i) if(p[a][i]!=p[b][i])a=p[a][i],b=p[b][i]; return p[a][0]; } void dfs2(int u) { for(int i=head[u];i;i=an[i].n) if(an[i].to!=fa[u]) { int v=an[i].to; dfs2(v); ji[u]+=ji[v]; } ans=max(ans,ji[u]); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<n;++i) scanf("%d%d",&a,&b),add(a,b),add(b,a); dfs(1,0); for(int i=1;i<=m;++i) { scanf("%d%d",&a,&b); c=getlca(a,b); ji[c]--; ji[fa[c]]--; ji[a]++;ji[b]++; } dfs2(1); cout<<ans; return 0; }
再來道難一點的。
NOIP2015運輸計划
公元 20442044 年,人類進入了宇宙紀元。
L 國有 n 個星球,還有 n-1 條雙向航道,每條航道建立在兩個星球之間,這 n-1 條航道連通了 L國的所有星球。
小 P 掌管一家物流公司, 該公司有很多個運輸計划,每個運輸計划形如:有一艘物流飛船需要從 u 號星球沿最快的宇航路徑飛行到 v 號星球去。顯然,飛船駛過一條航道是需要時間的,對於航道 j ,任意飛船駛過它所花費的時間為 t ,並且任意兩艘飛船之間不會產生任何干擾。
為了鼓勵科技創新, L國國王同意小 P的物流公司參與 L 國的航道建設,即允許小 P 把某一條航道改造成蟲洞,飛船駛過蟲洞不消耗時間。
在蟲洞的建設完成前小 P 的物流公司就預接了 m 個運輸計划。在蟲洞建設完成后,這 m 個運輸計划會同時開始,所有飛船一起出發。當這 m 個運輸計划都完成時,小 P 的物流公司的階段性工作就完成了。
如果小 P可以自由選擇將哪一條航道改造成蟲洞, 試求出小 P 的物流公司完成階段性工作所需要的最短時間是多少?
問題來了,剛才我們要求點的覆蓋次數,這回要求邊,我們都知道求點的覆蓋可以用樹鏈剖分來做,
但把點換成邊好像無從下手(不過好像也可以做。但復雜度不是太好看
那么我們該如何處理?
還是樹上差分,但我們把定義換一下,把邊上的信息放到點上,每個子樹記錄的是這個點向上連的那條邊出現的次數,左右端點分別加1,LCA減2,就可以搞了。
然后咧?
我們發現直接求很困難,所以就考慮把求最值改成二分答案+驗證找最值,先二分最終的答案,那么大於這個答案的鏈是需要刪邊的,在把需要刪邊的鏈來一波差分,找出這些鏈的最長公共鏈,判斷把這條鏈刪掉之后答案是否合法,然后這題就做完了。
#include<iostream> #include<cstdio> #include<cstring> #define N 300009 #define R register using namespace std; int tot,head[N],ji[N],jii[N],num,ll,lca[N],u[N],v[N],n,deep[N],d[N],fa[N],m,L[N],p[N][22]; int a,b,c,l,r; struct de { int n,to,l; }an[N<<1]; inline void add(int u,int v,int l) { an[++tot].n=head[u]; an[tot].to=v; head[u]=tot; an[tot].l=l; } void dfs2(int u,int fa) { for(R int i=head[u];i;i=an[i].n) if(an[i].to!=fa) { int v=an[i].to; dfs2(v,u); ji[u]+=ji[v]; } if(ji[u]==num)ll=max(ll,jii[u]); } bool ch(int pos) { int pp=0; num=0;ll=0; memset(ji,0,sizeof(ji)); for(R int i=1;i<=m;++i) if(L[i]>pos) { ji[lca[i]]-=2; ji[u[i]]++; ji[v[i]]++; pp=max(pp,L[i]); num++; } dfs2(1,0); if(pp-ll<=pos)return 1; else return 0; } void dfs(int u,int f) { fa[u]=f; deep[u]=deep[f]+1; p[u][0]=f; for(R int i=1;(1<<i)<=deep[u];++i) p[u][i]=p[p[u][i-1]][i-1]; for(R int i=head[u];i;i=an[i].n) if(an[i].to!=f) { int v=an[i].to; d[v]=d[u]+an[i].l; jii[v]=an[i].l; dfs(v,u); } } inline int getlca(int a,int b) { if(deep[a]<deep[b])swap(a,b); for(R int i=20;i>=0;--i) if(deep[a]-(1<<i)>=deep[b])a=p[a][i]; if(a==b)return b; for(R int i=20;i>=0;--i) if(p[a][i]!=p[b][i])a=p[a][i],b=p[b][i]; return p[a][0]; } int rd() { int x=0; char c=getchar(); while(!isdigit(c))c=getchar(); while(isdigit(c)) { x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x; } int main() { n=rd();m=rd(); for(R int i=1;i<n;++i) a=rd(),b=rd(),c=rd(),add(a,b,c),add(b,a,c); dfs(1,0); for(R int i=1;i<=m;++i) { u[i]=rd();v[i]=rd(); lca[i]=getlca(u[i],v[i]); L[i]=d[u[i]]+d[v[i]]-2*d[lca[i]]; r=max(r,L[i]); } int ans=0; while(l<=r) { int mid=(l+r)>>1; if(ch(mid)) { ans=mid; r=mid-1; } else l=mid+1; } cout<<ans; return 0; }
來看最后一道 NOIP2016 天天愛跑步
這是我見過的最難的一道樹上差分題目,它的解法十分的巧妙。
在第w[j]秒觀察到,難道我還讓它動態的往上跳嗎?
當然不用,讓我們列一波式子。
因為這是一顆樹,它具有一些非常有用(惡心)的性質,就是鏈上的LCA,當w在s到LCA的路上時,有以下式子成立
deep[S]=w[x]+deep[x]
當w在LCA到t的路上時,有以下式子成立
deep[s]-2*d[lca(s,t)]=w[x]-deep[x]
由於二式不等價,所以我們要分開處理,由於在S到LCA的路上一式成立,在LCA到T時二式成立,但我們在處理兩種情況時要注意不能重復。
所以我們開兩個映射,第一個表示在一式情況下左邊的式子的結果對應了幾個x,第二個表示二式下左邊的式子的結果對應了幾個x。
然后怎么做?
還考慮樹上差分,我們可以理解為又有一種數在s出現,在lca的父親處消失,另一種樹在t出現,在LCA處消失,這兩種數對應了上述兩種式子,那我們遍歷整棵樹時,到達一個節點就把這個位置對應的結果加入映射,對於每個詢問的答案,就是遍歷以這個點為根的子樹前后右邊的式子的結果對應的映射的差。
#include<iostream> #include<cstdio> #include<vector> #include<map> #define N 300009 using namespace std; map<int,int>A,B; struct pai { int tag,tag2,num; }; vector<pai>ji[N]; int n,m,head[N],p[N][22],deep[N],fa[N],tot,a,b,w[N],ans[N]; struct dwd { int n,to; }an[N<<1]; inline void add(int u,int v) { an[++tot].n=head[u]; an[tot].to=v; head[u]=tot; } void dfs(int u,int f) { fa[u]=f; deep[u]=deep[f]+1; p[u][0]=f; for(int i=1;(1<<i)<=deep[u];++i) p[u][i]=p[p[u][i-1]][i-1]; for(int i=head[u];i;i=an[i].n) { int v=an[i].to; if(v!=f)dfs(v,u); } } inline int getlca(int a,int b) { if(deep[a]<deep[b])swap(a,b); for(int i=20;i>=0;--i) if(deep[a]-(1<<i)>=deep[b])a=p[a][i]; if(a==b)return b; for(int i=20;i>=0;--i) if(p[a][i]!=p[b][i])a=p[a][i],b=p[b][i]; return p[a][0]; } void dfs2(int u,int fa) { int p=A[deep[u]+w[u]],q=B[w[u]-deep[u]];//gai for(int i=head[u];i;i=an[i].n) if(an[i].to!=fa) { int v=an[i].to; dfs2(v,u); } for(int i=0;i<ji[u].size();++i) { if(ji[u][i].tag==1)A[ji[u][i].num]+=ji[u][i].tag2; else B[ji[u][i].num]+=ji[u][i].tag2; } ans[u]=B[w[u]-deep[u]]+A[deep[u]+w[u]]-q-p; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<n;++i) scanf("%d%d",&a,&b),add(a,b),add(b,a); dfs(1,0); for(int i=1;i<=n;++i) scanf("%d",&w[i]); for(int i=1;i<=m;++i) { int s,t,lca; scanf("%d%d",&s,&t); lca=getlca(s,t); ji[s].push_back(pai{1,1,deep[s]}); ji[fa[lca]].push_back(pai{1,-1,deep[s]}); ji[t].push_back(pai{2,1,deep[s]-2*deep[lca]}); ji[lca].push_back(pai{2,-1,deep[s]-2*deep[lca]}); } dfs2(1,0); for(int i=1;i<=n;++i) printf("%d ",ans[i]); return 0; }