————————————————18.4.18更新
有時我們會遇到這樣的問題:在一棵樹上,每次詢問兩點間路徑上的和或者是最值。但我們用搜索時,時間就會到O(n),這樣根本就完不成算法。但樹上剖分就可以縮短修改的時間。
樹上剖分的算法簡介
我們定義每個有兒子的節點有一個重兒子(great son),重兒子的選擇方式有很多種,但使算法時間最少的重兒子選擇方式是選擇所有兒子中的子樹的節點數最多的那個兒子。(經驗證明博主蒟蒻不知)
上圖就是一個樹剖的例子。接下來我們將每條路徑上的重兒子用重鏈鏈接,按重鏈順序dFS,爲每個節點定義一個新的id值(dfs序)。同時對於每個節點定義一個top值爲它所在的重鏈上深度最小的點的編號。下面上圖,理解一下:
接下來,我們驚奇地發現:重鏈上的編號是連續的!所以樹上所有的信息可以儲存在一個線段樹裏維護。修改時,我們在樹上用top縮短路徑,就可以降低時間複雜度了。
樹剖一些相關信息
平攤複雜度修改查詢O(log2(n)),初始化複雜度O(n)。
樹剖可以求lca,路徑最值,和。缺點也很明顯,不能動態維護樹,這時就要使用LCT了。但是靜態時樹剖的常數更小更優秀。
樹剖算法實現
樹剖需要用到兩次dfs:第一次是初始化樹,第二次是初始化重鏈的top值和id值
vector <LL> ma[100010]; //樹邊存儲
void init(LL x){ //第一次dfs,x表示搜索到x號點
LL maxx=0,bian=0; // maxx表示該點兒子中最大的size值
vector <LL> ::iterator it;
dep[x]=dep[fa[x]]+1;
size[x]=1; //該點的size值先初始化
for(it=ma[x].begin();it!=ma[x].end();it++)if(*it!=fa[x]){
fa[*it]=x;init(*it);size[x]+=size[*it]; //先處理它的兒子,再計算出該點的size值
if(maxx<size[*it]){
maxx=size[*it];bian=*it; //更新重兒子
}
}
gs[x]=bian; //重兒子賦值
}
void bfs(LL x,LL t) { //第二次dfs ,x表示找到第x號點,t表示該點的top值
vector <LL> ::iterator it;
if(!x)return ;
top[x]=t;
id[x]=++cnt; //id爲該點的dfs序
fanid[id[x]]=x; //更新該id的反映射
bfs(gs[x],t);//先更新重兒子的top值,它的top值爲當前點的top值
for(it=ma[x].begin(); it!=ma[x].end(); it++)if(*it!=fa[x]&&*it!=gs[x])bfs(*it,*it);//它其他兒子的top值爲其它節點的編號
}
修改:某節點到另一節點的路徑所有點權加a
void mo(LL x,LL y,LL a){ // 修改x點到y點路徑上所有點的點權,使它們增加a
while(top[x]!=top[y]){ //將x,y先跳至同一重鏈
if(dep[top[x]]<dep[top[y]])swap(x,y); //先將x與y選擇一個跳至更大的深度
modify(1,id[top[x]],id[x]); //修改路徑上的所有點權
x=fa[top[x]]; //x跳至它top的爸爸
}
if(dep[x]<dep[y])swap(x,y);//跳至同一重鏈後,它們的路徑也要修改 ,選擇深度更大的
modify(a,id[y],id[x]); //修改它們路徑上的所有點權
}
這裏給大家留一個思考:如何修改某節點的子樹上所有節點的點權呢?提示:bfs序,看前面的圖找找區間規律吧
查詢:某節點到另一節點的路徑所有點權和
LL loadsum(LL x,LL y) { //查詢 x點到y點路徑上所有點的點權和
LL sum=0;
while(top[x]!=top[y]) {
if(dep[top[y]]>dep[top[x]])swap(x,y);
sum+=getsum(1,id[top[x]],id[x]); //加上路徑上所有點權和
x=fa[top[x]];
} //其實與之前修改操作的跳點操作一模一樣
if(dep[x]>dep[y])swap(x,y);
sum+=getsum(1,id[x],id[y]); //加上它們路徑上所有點權和
return sum;
}