先拋出一個問題,一棵n個點的樹,每個點有一個不變的權值,m次詢問任意兩點之間的權值和。
最簡單的算法:m次循環,每次從一個點出發,dfs累計走過的路徑,直到到達另外一個點。
int dfs(int x,int fa,long long deep)
{
d[x]=deep;
for(int i=head[x];i;i=nxt[i])
{
int u=to[i];if(u==fa) continue;
dfs(u,x,deep+w[i]);
}
}
如果你會前綴和,將其利用到樹上,將一條條鏈想象成數組,於是乎我們就可以快速求出在一條鏈上的兩點之間的權值和。
對於不在一條鏈上的點怎麼辦呢?我們求一下兩點(a,b)的LCA,將(a,b)分爲(lca,a)和(lca,b),這樣lca和每一個點都在一條鏈上了。
void get_treesum(int x,int fa)
{
sum[x]=sum[fa]+a[x];
for(int i=head[x];i;i=nxt[i])
{
int u=to[i];if(u==fa) continue;
get_treesum(u,x);
}
}
long long get_ans(int x,int y,int lca)
{
return sum[x]+sum[y]-sum[lca]-sum[fa[lca]];
}
如果還需要你支持一種操作:修改任意一個點的權值
我們還是先考慮處理的是數組,對於數組,我們不能再使用前綴和了,否則每次修改都是o(n)的,我們就可以使用線段樹或者樹狀數組來維護這個數組,做到查詢和修改都是o(logn)。
那對於樹,可以把樹看成很多數組組成的,那麼我們可以直接套用線段樹麼?
顯然不可以的,因爲要用多棵線段樹維護,那麼單次修改的複雜度可能高達o(nlogn)。
那麼可否用一棵線段樹進行維護呢?我們姑且隨便編一下號。id[x]表示x點在線段樹上的位置。
對於修改o(logn),但是查詢需要一個個點上去搜o(nlogn)。
顯然線段樹上單點操作和區間操作的複雜度是相同的,而我們每次操作或者查詢的點的數量是一定的。
所以我們需要儘量多的區間操作,儘量少的單點操作使我們的總複雜度最小。
那麼我們對於一條鏈的處理,最簡單的就是從上到下id[x]依次增加,那麼我們處理這條鏈的時候只需要一次區間操作。
但是問題是我們有很多鏈的處理,不可能滿足每一條鏈自上而下的id都是依次增加的,只能挑選部分的鍊形成連續的id。
那我們肯定儘量挑選連續鏈 儘量長一些,這樣造成區間處理的點可以更多,
比如說挑選的這條鏈從根節點出發延申,顯然可以延申到深度最大的葉節點結果更優。
那麼我們每次dfs到一個點,就將它節點數最大的兒子標記爲重兒子。
void dfs1(int x,int fa)
{
son[x]=1;dep[x]=dep[fa]+1;
for(int i=head[x];i;i=nxt[i])
{
int u=to[i];if(u==fa) continue;
dfs1(u,x);son[x]+=son[u];
if(son[u]>son[wson[x]]) wson[x]=son[u];
}
}
在第二次dfs的時候,我們主要進行兩個操作,一個是將x(dfs到的點)給一個線段樹上的位置編號(該編號滿足在dfs2完之後很多鏈的編號是連續的,實現線段樹上儘可能多的區間處理),另一個操作是維護每個點的鏈首是誰。
鏈首是什麼?就是你要處理形成的鏈的頂端(暫時不理解也沒關係)。
void dfs2(int x,int topp)
{
topf[x]=topp;id[x]=++cnt;
if(wson[x]) dfs2(wson[x],topp);
for(int i=head[x];i;i=nxt[i])
{
int u=to[i];if(u==wson[x]||u==fa[x]) continue;
dfs2(u,u);
}
}
處理完之後可以得到類似下圖,藍字是id[],紅點表示鏈首,粗線表示重鏈,細線表示輕鏈。
這樣我們任意取一個點到根節點,可以發現處理次數均不大於3次。
下面給出從x到根節點最多經過2*logn條鏈的簡單證明:
1.假設上方全部是輕鏈,那麼假設當前走過一條輕鏈u→v(fa[u]==v),必定存在v的一個兒子的節點數大於son[u],那麼走過這條輕鏈的時候,我們就已經至少有2*son[u]個點不需要操作了,每跳一次輕鏈就少處理當前兒子數的兩倍,所以最多處理logn次。
2.假設上方全是重鏈,只需要一次線段樹操作。
3.假設又有重鏈又有輕鏈,那麼我們處理的不是重鏈就是輕鏈,處理完一次重鏈上面的鏈必定是輕鏈(如果重鏈上面是重鏈,那麼他們顯然可以連接在一起處理一次),所以走重鏈的次數和約等於走輕鏈的次數,那麼最糟糕的情況就是走logn次輕鏈和logn次重鏈。
樹鏈剖分非常依賴線段樹的操作,線段樹用的越好,鏈剖用的越好,下面是建線段樹的操作。
#define lson o<<1
#define rson (o<<1)|1
void build_tree(int o,int l,int r)
{
if(l==r) {t[o]=a[l];return ;}
int mid=(l+r)/2;
build_tree(lson,l,mid);build_tree(rson,mid+1,r);
}
t[o]中的o對應的是線段樹的節點編號,而a[l]的l代表線段樹的位置編號,所以我們需要在dfs2中加入下列操作:
topf[x]=topp;id[x]=++cnt;a[cnt]=v[x];
v[x]對應的是真實樹上的x點的權值,x在線段樹上的位置編號是cnt,所以a[]是過渡數組,將線段樹節點編號和點權值相聯繫。
下面是修改真實樹x節點的權值改爲y(實際上就是線段樹的單點修改):
void c_tree(int o,int l,int r,int pos,int val)
{
if(l==r) {t[o]=val;return ;}
int mid=(l+r)/2;
if(pos<=mid) c_tree(lson,l,mid,pos,val);
else c_tree(rson,mid+1,r,pos,val);
t[o]=t[lson]+t[rson];
}
c_tree(1,1,n,id[x],y);
下面是我們查詢一條鏈權值和的操作,因爲是一條處理後的鏈,所以id是連續的(l,l+1,l+2......r-2,r-1,r),下面的查詢操作:
LL quiry_tree(int o,int l,int r,int ll,int rr)
{
if(ll<=l&&rr>=r) return t[o];
int mid=(l+r)/2;LL ans=0;
if(ll<=mid) ans+=quiry_tree(lson,l,mid,ll,rr);
if(rr>mid) ans+=quiry_tree(rson,mid+1,r,ll,rr);
return ans;
}
那麼我們現在處理兩個點之間的權值和,我們已經可以處理一條鏈的權值和,我們現在只需要把兩點之間的線段分成很多條鏈,我們兩個點同時處理,每次先把 鏈首 深度 大的鏈先處理,然後該點跳到鏈首的父親位置(點的位置都是沒有處理的點),直到他們都跳到同一條鏈中(請想象),再處理這條鏈即可。
LL quiry_road(int x,int y)
{
LL ans=0;
while(topf[x]!=topf[y]) //不是同一條鏈就跳
{
if(dep[topf[x]]<dep[topf[y]]) swap(x,y); //每次處理鏈首深度大的
ans+=quiry_tree(1,1,n,id[topf[x]],id[x]);
x=fa[topf[x]];
}
if(dep[x]<dep[y]) swap(x,y);
ans+=quiry_tree(1,1,n,id[y],id[x]);
return ans;
}