樹上的簡單操作 樹鏈剖分

2022-08-29 05:27:10 字數 3943 閱讀 5833

某神犇:樹鏈剖分什麼垃圾,能做的lct都能做,不能做的lct也能做

線段樹,(都會線段樹了應該知道什麼是樹吧)

現在考慮一棵樹,每個節點都有乙個點權,要求給x到y路徑上的點都加上k,這個問題可以用樹上差分很簡單地在o(m+n)的複雜度內解決。再考慮乙個問題,要求查詢樹上x到y這條路徑上的權值和,也可以先求出每個點到根的dis,然後求出x和y的lca,最後用公式:dis(x,y)=dis(x,root)+dis(y,root)-2*dis(lca,root)簡單地在o(mlogn+n)的時間內解決。那如果我們把著兩種操作整合在一起呢?樹鏈剖分就這麼誕生出來了。

樹鏈剖分,顧名思義,就是把一棵樹殘忍地剖成一條一條鏈,然後通過鏈之間的特性來用某些資料結構去維護它們。我們剖樹的時候通常會遵循兩大準則:重鏈剖分和實鏈剖分,本文暫時只討論重鏈剖分。

一般來講樹鏈剖分的碼量都很大,所以可以看作是一種模擬

說到重鏈,我們先談談什麼是重兒子。對於某乙個樹上節點u,它的重兒子就是它兒子裡面那個size最大的兒子。可以看成,乙個節點只能有乙個重兒子,而其他的兒子被稱作輕兒子。又重兒子組成的鏈叫做重鏈,由輕兒子組成的鏈叫做輕鏈。

來張圖康康,重鏈都被加粗顯示了:

在這張圖裡面,我們可以看到,1的兒子中3的size更大,所以1->3就被劃分成了一條重鏈,同樣,雖然1->2不是重鏈,但是2->5也可以是重鏈這樣整棵樹就被劃分成了重鏈和輕鏈。

首先,我們先宣告一些變數:

int size[maxn],dep[maxn],f[maxn],hson[maxn];
size就是子樹大小,dep是節點深度,f是節點父親,hson是節點的重兒子

接著,我們開始寫第乙個dfs:

void dfs1(int u,int fa,int d)

}

整個程式還是非常簡單的,我們已經處理出來了整棵樹的一些基本資訊,那麼我們現在就要開始把鏈整合在一起了。

第二個dfs: 這裡我們引入乙個dfs序的東西。

其實也蠻簡單,就是對於某乙個節點u,它在dfs的過程中被訪問到的順序,再來張圖:

可能有人會問,圖是不是錯了啊,因為id[2]不應該是2嗎,為什麼id[3]是2啊?因為我們有乙個規定,在第二次dfs的時候優先對重兒子進行搜尋。因為我們必須保證任何一條重鏈上的點的dfs序是連續的,如果我們優先搜尋2,那麼3的dfs序就是5了,和1的dfs序不連續,也就失去了意義。我們還要對每一條重鏈進行標識。或者是說,對於某乙個點u,我們要給出它所在的重鏈的頂部節點(輕節點的頂部節點是其本身),這裡我們用top[u]來表示頂部節點。再最後,我們把點值也轉移到另外乙個陣列裡面。具體細節看程式吧,反正也不是很長:

int id[maxn],wt[maxn],top[maxn],cnt=0;

void dfs2(int u,int tc)t[maxn*2];

ll ans=0;

void update(ll pos)

void build(ll l,ll r,ll pos)

ll mid=(l+r)/2;

build(l,mid,pos<<1);

build(mid+1,r,pos<<1|1);

update(pos);

}void change(ll pos,ll l,ll r,ll k)

void pushdown(ll l,ll r,ll pos)

void add(ll tl,ll tr,ll l,ll r,ll v,ll pos)

if(rtr)

ll mid=(l+r)/2;

pushdown(l,r,pos);

add(tl,tr,l,mid,v,pos<<1);

add(tl,tr,mid+1,r,v,pos<<1|1);

update(pos);

}void query(ll tl,ll tr,ll l,ll r,ll pos)

if(rtr)

ll mid=(l+r)/2;

pushdown(l,r,pos);

query(tl,tr,l,mid,pos<<1);

query(tl,tr,mid+1,r,pos<<1|1);

return;

}

那麼,對於某乙個詢問x和y之間的點值之和的詢問,我們可以把它分成兩部分:

x到top[x]的重鏈區間和

top[x]到top[y]的輕鏈和

top[y]到y的重鏈和

其實現實中情況比這個複雜,打個比方,有一種很奇怪的食物,兩塊麵包中間由一根麵條連線(?)我們可以一口吃掉一塊麵包o(logn),但是吃麵條要用到o(n),那麼我們最簡單的想法就是從這種奇怪的食物的某乙個節點id[x]吃到id[top[x]]來吃掉乙個麵包(重鏈),然後從id[top[x]]到f[top[x]]去吃掉一根麵條(輕鏈),就這麼下去直到吃掉最後一塊麵包。程式如下:

int c_ask(int x,int y)
(由於我線段樹板子寫的太噁心所以要一遍一遍地重置ans值,但是我懶得改了)

鏈上修改也很簡單,照著套就完事了:

void c_add(int x,int y,int val)
再就是子樹修改:

add(id[x],id[x]+size[x]-1,1,n,v%mod,1);
因為實際上一棵子樹的dfs序也是連續的,可以自己手動模擬一下,所以就是簡單地加上size[x]-1就好了

子樹查詢:

query(id[x],id[x]+size[x]-1,1,n,1);
一道模板題:

ac**:

#include #define ll long long

using namespace std;

const int maxn=2*1e6+10;

vectorgpe[maxn];

int a[maxn],n,m,r,mod;

int size[maxn],dep[maxn],f[maxn],hson[maxn];

void dfs1(int u,int fa,int d)

}int id[maxn],wt[maxn],top[maxn],cnt=0;

void dfs2(int u,int tc)

if(rtr)

ll mid=(l+r)/2;

pushdown(l,r,pos);

add(tl,tr,l,mid,v,pos<<1);

add(tl,tr,mid+1,r,v,pos<<1|1);

update(pos);

}void query(ll tl,ll tr,ll l,ll r,ll pos)

if(rtr)

ll mid=(l+r)/2;

pushdown(l,r,pos);

query(tl,tr,l,mid,pos<<1);

query(tl,tr,mid+1,r,pos<<1|1);

return;

}int c_ask(int x,int y)

void c_add(int x,int y,int val)

int main(void)

for(int i=1;i<=n-1;i++)

dfs1(r,r,1);

dfs2(r,r);

build(1,n,1);

while(m--)else if(opt==2)else if(opt==3)else

}}

假設(u,v)是一條輕邊,那麼size(v)所以,樹鏈剖分的時間複雜度是o(nlog^2n)

HAOI2015 樹上操作 樹鏈剖分

1963.haoi2015 樹上操作 有一棵點數為n的樹,以點1為根,且樹點有權值。然後有m個操作,分為三種 操作1 把某個節點x的點權增加a。操作2 把某個節點x為根的子樹中所有點的點權都增加a。操作3 詢問某個節點x到根的路徑中所有點的點權和。第一行兩個整數n,m,表示點數和運算元。接下來一行n...

HAOI2015 樹上操作 樹鏈剖分

有一棵點數為n的樹,以點1為根,且樹點有權值。然後有m個操作,分為三種 操作1 把某個節點x的點權增加a。操作2 把某個節點x為根的子樹中所有點的點權都增加a。操作3 詢問某個節點x到根的路徑中所有點的點權和。第一行兩個整數n,m,表示點數和運算元。接下來一行n個整數,表示樹中節點的初始權值。接下來...

簡單題 最小生成樹, 樹鏈剖分, 樹上差分

將最小生成樹構建出來,將沒加入最小生成樹的邊稱為 虛邊 假設有一條 虛邊 為 u,v u,vu,v,則 u,v u,vu,v 在樹上的路徑上的邊可能被替換,稱路徑上的邊被覆蓋 同上 include define reg register define pb push back intread whi...