P1600 天天愛跑步(樹上差分,桶思想,最近公共祖先)

P1600 天天愛跑步

題目描述

給定一棵樹和一些路徑,每個點有一個點權,求每個點點權恰好等於路徑上的訪問排位的數量.

舉個例子:s>ts->t經過了點kk,點kk的權值爲ww且點kk恰好是這條路徑上第w+1w+1個點(路徑上第一個點ss的訪問排位爲0),則ans[k]++ans[k]++.

題目分析

25pts:

隨便亂搞都可以. 我採用的方法是在線處理每條路徑,用樹剖的方法把路徑剖下來,然後再與標準比較,如果ww和次序相同那麼這個點的ans++ans++,時間複雜度O(nmlog2n)O(nmlog^2n). 能過25%25\%的數據.

評測記錄

100pts:

我們需要轉換思路. 每次的一條鏈不能對每一個元素都進行操作,否則時間快不上來.

那我們就想到了一種更加簡單的對樹上區間進行操作的方法

樹上差分:

關於差分和樹上差分的淺談

差分通常是和前綴和結合起來的,比如說以上博文的例子,如果覆蓋了,那麼在區間前端加一,在區間末端減一.

這樣,我們就相當於給區間打了標記,不用每次給出一段區間時都處理,而是最後再使用一個差分數組,統一處理標記,複雜度自然就下來了.

樹上差分也是一樣. 通過打標記的方式,來把對區間處理的複雜度由O(n)O(n)降爲O(1)O(1).

比如說,我們有這樣一條路徑

在這裏插入圖片描述

對於這條路徑,我們這樣打上標記

在這裏插入圖片描述

我們在計算一個點被多少條路徑經過時,只要統計在它的子樹中的標記的和就可以了.

值得注意的是,兩個-1的標記,一個打在lcalca上,另一個打在fa[lca]fa[lca]上,這樣可以保證lcalca不會因爲是兩段的交集而重複計算路徑數.

這樣子,我們把原來每條路徑上的點處理一次,總的處理複雜度爲O(n2)O(n^2),變成了每條路徑進行O(1)O(1)處理,最後再用一次dfsdfs O(n)O(n)算出每個點的覆蓋次數,從而大大降低了時間複雜度.

不難發現,我們同樣可以把以上差分的處理方法運用到這道題上.


桶思想:

簡而言之,桶就是不論裏面的東西是什麼,而只看裏面的東西有多少.

關於桶這一思想,可以參見另外一篇博客:(留坑待填)

我們首先要對這一題的題目條件進行轉換.

不難發現,對於任意一個在某一條路徑上的點xx,如果這條路徑對這個點的答案有貢獻,那麼必定滿足以下條件之一:

1.如果這個點在s>lcas->lca上,那麼dep[x]dep[u]=w[x]dep[x]-dep[u]=w[x]
2.如果這個點在lca>tlca->t上,那麼dep[u]dep[lca]+dep[x]dep[lca]=w[x]dep[u]-dep[lca]+dep[x]-dep[lca]=w[x].

由以上兩個條件,可以發現:

對於式1,dep[u]=w[x]+dep[x]dep[u]=w[x]+dep[x],等號右邊是定值,由上面的差分思路可知,我們只要找到所有在xx的子樹中且深度爲dep[u]dep[u]的點的數量即可.
對於式2,dep[u]2dep[lca]=w[x]dep[x]dep[u]-2*dep[lca]=w[x]-dep[x],等號右邊是定值,這裏不方便找點,我們改爲找一個桶的大小,只需要找到在子樹中值爲dep[u]2dep[lca]dep[u]-2*dep[lca]桶的大小即可.

那麼思路就很明顯了,我們開兩組桶,每個桶裏面儲存的是恰好滿足條件的路徑的數量,比如說bucket1[dep]bucket1[dep]就表示了起始點深度爲depdep的路徑的多少,bucket2[dep]bucket2[dep]表示了滿足dep[u]2dep[lca]dep[u]-2*dep[lca]的路徑的數量多少.

爲什麼要開兩個桶數組呢?因爲對於每個點xx,我們要查詢的桶都有兩個,w[x]+dep[x]w[x]+dep[x]w[x]dep[x]w[x]-dep[x].

此外,第二個數組可能發生dep[u]2dep[lca]<0dep[u]-2*dep[lca]<0的情況,爲了防止這種情況出現,我們還需要給第二個桶的容量統一加上maxnmaxn.


到這裏這道題就基本做完了. 倍增求lca是樹上差分的套路就不講了 但是還需要考慮一些其他的東西:

1.當一個點同時滿足以上兩個式子的時候,ansans會重複計算,多算一次路徑的貢獻. (這裏和普通樹上差分不太一樣)

上面的兩個式子
dep[u]2dep[lca]=w[x]dep[x]dep[u]-2dep[lca]=w[x]-dep[x]
dep[u]=w[x]+dep[x]dep[u]=w[x]+dep[x]
因爲dep[u]dep[u]相同,所以聯立得
w[x]dep[x]+2dep[lca]=w[x]+dep[x]w[x]-dep[x]+2dep[lca]=w[x]+dep[x]
dep[x]=dep[lca]dep[x]=dep[lca],也就是說,如果lcalca滿足其中一個式子,它必定滿足另一個,所以如果lcalca滿足一個式子,ans[lca]ans[lca]
要-1,以抵消多算一次的影響.

2.一個點的ansans應該取未訪問它的子樹之前和訪問它的子樹之後的差值,以防止非同一子樹的桶的元素造成的影響.

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.對於樹上的區間操作:

區間是否便於用線段樹維護?是則使用樹鏈剖分,否則使用差分或者差分思想.

使用差分思想:題意給出的(可能隱藏的)式子能否轉換成便於差分維護/便於用差分思想維護(這種情況下通常是桶)的形式?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章