題目描述
給定一棵樹和一些路徑,每個點有一個點權,求每個點點權恰好等於路徑上的訪問排位的數量.
舉個例子:經過了點,點的權值爲且點恰好是這條路徑上第個點(路徑上第一個點的訪問排位爲0),則.
題目分析
25pts:
隨便亂搞都可以. 我採用的方法是在線處理每條路徑,用樹剖的方法把路徑剖下來,然後再與標準比較,如果和次序相同那麼這個點的,時間複雜度. 能過的數據.
100pts:
我們需要轉換思路. 每次的一條鏈不能對每一個元素都進行操作,否則時間快不上來.
那我們就想到了一種更加簡單的對樹上區間進行操作的方法
樹上差分:
差分通常是和前綴和結合起來的,比如說以上博文的例子,如果覆蓋了,那麼在區間前端加一,在區間末端減一.
這樣,我們就相當於給區間打了標記,不用每次給出一段區間時都處理,而是最後再使用一個差分數組,統一處理標記,複雜度自然就下來了.
樹上差分也是一樣. 通過打標記的方式,來把對區間處理的複雜度由降爲.
比如說,我們有這樣一條路徑
對於這條路徑,我們這樣打上標記
我們在計算一個點被多少條路徑經過時,只要統計在它的子樹中的標記的和就可以了.
值得注意的是,兩個-1的標記,一個打在上,另一個打在上,這樣可以保證不會因爲是兩段的交集而重複計算路徑數.
這樣子,我們把原來每條路徑上的點處理一次,總的處理複雜度爲,變成了每條路徑進行處理,最後再用一次 算出每個點的覆蓋次數,從而大大降低了時間複雜度.
不難發現,我們同樣可以把以上差分的處理方法運用到這道題上.
桶思想:
簡而言之,桶就是不論裏面的東西是什麼,而只看裏面的東西有多少.
關於桶這一思想,可以參見另外一篇博客:(留坑待填)
我們首先要對這一題的題目條件進行轉換.
不難發現,對於任意一個在某一條路徑上的點,如果這條路徑對這個點的答案有貢獻,那麼必定滿足以下條件之一:
1.如果這個點在上,那麼;
2.如果這個點在上,那麼.
由以上兩個條件,可以發現:
對於式1,,等號右邊是定值,由上面的差分思路可知,我們只要找到所有在的子樹中且深度爲的點的數量即可.
對於式2,,等號右邊是定值,這裏不方便找點,我們改爲找一個桶的大小,只需要找到在子樹中值爲桶的大小即可.
那麼思路就很明顯了,我們開兩組桶,每個桶裏面儲存的是恰好滿足條件的路徑的數量,比如說就表示了起始點深度爲的路徑的多少,表示了滿足的路徑的數量多少.
爲什麼要開兩個桶數組呢?因爲對於每個點,我們要查詢的桶都有兩個,和.
此外,第二個數組可能發生的情況,爲了防止這種情況出現,我們還需要給第二個桶的容量統一加上.
到這裏這道題就基本做完了. 倍增求lca是樹上差分的套路就不講了 但是還需要考慮一些其他的東西:
1.當一個點同時滿足以上兩個式子的時候,會重複計算,多算一次路徑的貢獻. (這裏和普通樹上差分不太一樣)
上面的兩個式子
因爲相同,所以聯立得
,
,也就是說,如果滿足其中一個式子,它必定滿足另一個,所以如果滿足一個式子,
要-1,以抵消多算一次的影響.
2.一個點的應該取未訪問它的子樹之前和訪問它的子樹之後的差值,以防止非同一子樹的桶的元素造成的影響.
3.桶的大小要開夠.
程序實現
#include<bits/stdc++.h>
#define maxn 300010
using namespace std;
struct edge{
int v,next;
}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v){
e[++tot].v =v;
e[tot].next =head[u];
head[u]=tot;
}
int dep[maxn],fa[maxn][30];
void dfs1(int u,int pre){
dep[u]=dep[pre]+1;
fa[u][0]=pre;
for(int i=head[u];i;i=e[i].next ){
int v=e[i].v ;
if(v==pre)continue;
dfs1(v,u);
}
}
int LCA(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int i=20;i>=0;i--){
if(dep[fa[u][i]]>=dep[v])u=fa[u][i];
}
if(u==v)return u;
for(int i=20;i>=0;i--){
if(fa[u][i]==fa[v][i])continue;
u=fa[u][i],v=fa[v][i];
}
return fa[u][0];
}
int ans[maxn],wt[maxn];
map<int ,int >add_start[maxn],add_to[maxn],lca_start[maxn],lca_to[maxn];
int bucket1[maxn<<1],bucket2[maxn<<1];
int sums[maxn],sumt[maxn],minus1[maxn],minus2[maxn];
void dfs2(int u,int pre){
int t1=bucket1[wt[u]+dep[u]],t2=bucket2[wt[u]-dep[u]+maxn];
for(int i=head[u];i;i=e[i].next ){
int v=e[i].v ;
if(v==pre)continue;
dfs2(v,u);
}
for(int i=1;i<=sums[u];i++)bucket1[add_start[u][i]]++;//差分爲了保證準確性,通常都是先加後減
for(int i=1;i<=sumt[u];i++)bucket2[add_to[u][i]]++;//相應的桶++
ans[u]+=bucket1[wt[u]+dep[u]]-t1+bucket2[wt[u]-dep[u]+maxn]-t2;//計算增量
for(int i=1;i<=minus1[u];i++)bucket1[lca_start[u][i]]--;
for(int i=1;i<=minus2[u];i++)bucket2[lca_to[u][i]]--;//相應的桶--
}
int n,m;
int main(){
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<n;i++){
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
for(int i=1;i<=n;i++)scanf("%d",&wt[i]);
dfs1(1,1);
for(int j=1;j<=20;j++)
for(int i=1;i<=n;i++)
fa[i][j]=fa[fa[i][j-1]][j-1];
for(int i=1,s,t,lca;i<=m;i++){
scanf("%d%d",&s,&t);
lca=LCA(s,t);
add_start[s][++sums[s]]=dep[s];//用map存:在這個點的位置桶dep[s]++
add_to[t][++sumt[t]]=dep[s]-2*dep[lca]+maxn;//同上
lca_start[lca][++minus1[lca]]=dep[s];//這個桶在lca的位置--
lca_to[lca][++minus2[lca]]=dep[s]-2*dep[lca]+maxn;
if(dep[lca]+wt[lca]==dep[s])ans[lca]--;//如果一個點是lca又滿足條件,則要防止重複計算路徑
}
dfs2(1,1);
for(int i=1;i<=n;i++)printf("%d ",ans[i]);
return 0;
}
題後總結
1.桶是非常重要的一種思想,桶可以用來存滿足某個條件的數的數量,比較方便.
但是使用桶,就要求數據範圍不是很大,否則佔用空間就太多了.
對於全局桶的應用:記錄它對某次計算的貢獻可以統計他的增量.
2.對於樹上的區間操作:
區間是否便於用線段樹維護?是則使用樹鏈剖分,否則使用差分或者差分思想.
使用差分思想:題意給出的(可能隱藏的)式子能否轉換成便於差分維護/便於用差分思想維護(這種情況下通常是桶)的形式?