樹鏈剖分 — 輕重邊路徑剖分
樹鏈,就是樹上的路徑。剖分,就是把路徑分類為重鏈和輕鏈。
記siz[v]表示以v為根的子樹的節點數,dep[v]表示v的深度(根深度為1),top[v]表示v所在的重鏈的頂端節點,fa[v]表示v的父親,son[v]表示與v在同一重鏈上的v的兒子節點(姑且稱為重兒子),w[v]表示v與其父親節點的連邊(姑且稱為v的父邊)線上段樹中的位置。只要把這些東西求出來,就能用logn的時間完成原問題中的操作。
重兒子:siz[u]為v的子節點中siz值最大的,那麼u就是v的重兒子。
輕兒子:v的其它子節點。
重邊:點v與其重兒子的連邊。
輕邊:點v與其輕兒子的連邊。
重鏈:由重邊連成的路徑。
輕鏈:輕邊。
剖分後的樹有如下性質:
性質1:如果(v,u)為輕邊,則siz[u] * 2 < siz[v];
性質2:從根到某一點的路徑上輕鏈、重鏈的個數都不大於logn。
首先我們有一顆樹每個點(或者邊)有權值,我們要做的就是詢問兩個點之間路徑上各點(邊)權值的最大、最小,權值和(就是線段樹能幹的),然後我們還要支援線上更改任意節點(邊)的權值。
我們要做的是輕重鏈剖分,首先我們看幾個定義
size:和SBT裡的一樣,size[i]為以該點為根節點的子樹一共有幾個節點。
重兒子:一個節點當不為葉子節點的時候有且只有一個重兒子,重兒子為該點的兒子中size最大的,有多個最大時任選一個。
重鏈:由根節點開始,每個點每次都訪問自己的重兒子,一直訪問到葉子節點,就組成了一條重鏈
那麼對於一個點的非重兒子來說,以他為根節點,可以重新訪問出一條重鏈
如圖所示,用紅色的線畫出的為重鏈,其中6號點自己為一條重鏈,那麼對於每條重鏈,我們需要記下他的頂標top,就是該重鏈中深度最小的節點的標號
比如鏈1-3-4-9-10,的top為1,鏈2-8的top為2。
重鏈幾個明顯的性質就是互不重合且所有重鏈覆蓋所有點,重鏈之間由一條不在重鏈上的邊(我們稱作輕邊)連線
那麼我們首先DFS,可以求出每個點的size,然後再深搜一遍可得到每個點的top,和處理出每一條鏈。
有了所有的重鏈,覆蓋每一個點,然後我們要處理的問題是兩點之間的最值等問題。
有些像線段樹,假設我們需要求一條重鏈上的最大值,那麼我們需要將重鏈存進線段樹,且重鏈上的所有點的編號是連續的(區間),那麼我們要對所有的點以深搜序重新編號(dfs進行樹鏈剖分的時候順便標記一下就好了),那麼我們可以用線段樹來存樹上點的值(邊的權值也一樣,可以將邊下放到點 **重要思想**),這樣對於在同一條重鏈上的點我們就可以在logn的時間裡求出值了,然後對於不同鏈上的點,我們先給他們升到同一深度上的鏈,同時更新答案,然後做就好了(上升到同一深度上的重鏈這個過程有點類似於LCA倍增,noip2013 Day1 T3 火車運輸,其實這道題用樹鏈剖分的水題)至於每個點的權值修改就是線段樹的事了。
演算法實現:
我們可以用兩個dfs來求出fa、dep、siz、son、top、w。dfs_1:把fa、dep、siz、son求出來,比較簡單,略過。
dfs_2:
⒈對於v,當son[v]存在(即v不是葉子節點)時,顯然有top[son[v]] = top[v]。線段樹中,v的重邊應當在v的父邊的後面,記w[son[v]] = totw+1,totw表示最後加入的一條邊線上段樹中的位置。此時,為了使一條重鏈各邊線上段樹中連續分佈,應當進行dfs_2(son[v]);
⒉對於v的各個輕兒子u,顯然有top[u] = u,並且w[u] = totw+1,進行dfs_2過程。這就求出了top和w。
將樹中各邊的權值線上段樹中更新,建鏈和建線段樹的過程就完成了。
修改操作:例如將u到v的路徑上每條邊的權值都加上某值x。
一般人需要先求LCA,然後慢慢修改u、v到公共祖先的邊。而高手就不需要了。
記f1 = top[u],f2 = top[v]。
當f1 <> f2時:不妨設dep[f1] >= dep[f2],那麼就更新u到f1的父邊的權值(logn),並使u = fa[f1]。
當f1 = f2時:u與v在同一條重鏈上,若u與v不是同一點,就更新u到v路徑上的邊的權值(logn),否則修改完成;
重複上述過程,直到修改完成。
求和、求極值操作:類似修改操作,但是不更新邊權,而是對其求和、求極值。
就這樣,原問題就解決了。鑑於鄙人語言表達能力有限,咱畫圖來看看:
如圖所示,較粗的為重邊,較細的為輕邊。節點編號旁邊有個紅色點的表明該節點是其所在鏈的頂端節點。邊旁的藍色數字表示該邊線上段樹中的位置。圖中1-4-9-13-14為一條重鏈。
當要修改11到10的路徑時。
第一次迭代:u = 11,v = 10,f1 = 2,f2 = 10。此時dep[f1] < dep[f2],因此修改線段樹中的5號點,v = 4, f2 = 1;
第二次迭代:dep[f1] > dep[f2],修改線段樹中10--11號點。u = 2,f1 = 2;
第三次迭代:dep[f1] > dep[f2],修改線段樹中9號點。u = 1,f1 = 1;
第四次迭代:f1 = f2且u = v,修改結束。
**資料規模大時,遞迴可能會爆棧,而非遞迴dfs會很麻煩,所以可將兩個dfs改為寬搜+迴圈。即先寬搜求出fa、dep,然後逆序迴圈求出siz、son,再順序迴圈求出top和w。
練手題目:spoj375。
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <string.h>
using namespace std;
const int maxn = 100010;
struct Tedge
{
int b, next;
} e[maxn*2]; // 存樹上的邊
int tree[maxn]; // 線段樹, 存重鏈
int CNT, n, tot, edge, root, a, b, c; // tot 記錄線段樹(區間上)點的數量
int d[maxn][3]; // d[][ from|to|weight ] d[][]陣列儲存n-1條邊的資訊
// first[] 每個點的鄰邊鏈式前向星儲存, dep[u]記錄在以root為根時,頂點在樹上的深度
// w[u]表示u在重鏈線段樹上的序號, top[u]記錄u所在重鏈的最小頂點標號
// son[]記錄每個頂點的重兒子編號, siz[u]頂點u的孩子節點數, fa[]記錄每個頂點的父親編號
int first[maxn], dep[maxn], w[maxn], fa[maxn], top[maxn], son[maxn], siz[maxn];
char ch[10];
void insert(int a, int b, int c) // 插入邊,建樹
{
e[++edge].b = b;
e[edge].next = first[a];
first[a] = edge;
}
void dfs(int v)
{
siz[v] = 1,son[v] = 0;
for (int i = first[v]; i > 0; i = e[i].next)
if (e[i].b != fa[v])
{
fa[e[i].b] = v;
dep[e[i].b] = dep[v]+1;
dfs(e[i].b);
if (siz[e[i].b] > siz[son[v]]) son[v] = e[i].b;
siz[v] += siz[e[i].b];
}
}
void build_tree(int v, int tp)
{
w[v] = ++tot, top[v] = tp;
if (son[v] != 0) // 非葉子節點必然含有重兒子,為保證重鏈線上段樹(區間)上標號連續
build_tree(son[v], top[v]); // 先遞迴建立重兒子
for ( int i = first[v]; i > 0; i = e[i].next)
if (e[i].b != son[v] && e[i].b != fa[v]) // 點的非重兒子,以他為根節點,可以重新訪問出一條重鏈
build_tree(e[i].b, e[i].b);
}
void update(int root, int left, int right, int pos, int x)
{
if (pos > right || left > pos) return;
if (left == right)
{
tree[root] = x;
return;
}
int mid = (left + right) / 2, ls=root*2, rs=ls+1;
update(ls, left, mid, pos, x);
update(rs, mid+1, right, pos, x);
tree[root] = max(tree[ls], tree[rs]);
}
int maxi(int root, int left, int right, int l, int r)
{
if (l > right || r < left) return 0;
if (l <= left && right <= r) return tree[root];
int mid = (left + right) / 2, ls = root * 2, rs = ls + 1;
return max(maxi(ls, left, mid, l, r), maxi(rs, mid+1, right, l, r));
}
inline int find(int va, int vb)
{
int f1 = top[va], f2 = top[vb], tmp = 0;
while (f1 != f2)
{
if (dep[f1] < dep[f2])
{
swap(f1, f2);
swap(va, vb);
}
tmp = max(tmp, maxi(1, 1, tot, w[f1], w[va]));
va = fa[f1];
f1 = top[va];
}
if (va == vb) return tmp;
if (dep[va] > dep[vb]) swap(va, vb);
return max(tmp, maxi(1, 1, tot, w[son[va]], w[vb])); // 由於樹上邊的權值是下放到點上,線段樹son[va]->vb
}
void init()
{
scanf("%d", &n);
root = (n + 1) / 2; // 將一棵無根樹轉為有根樹,其中取中間編號的節點為根 root
fa[root] = tot = dep[root] = edge = 0;
memset(siz, 0, sizeof(siz));
memset(first, 0, sizeof(first));
memset(tree, 0, sizeof(tree));
for (int i = 1; i < n; i++)
{
scanf("%d%d%d", &a, &b, &c);
d[i][0] = a; d[i][1] = b; d[i][2] = c;
insert(a, b, c);
insert(b, a, c);
}
dfs(root);
build_tree(root, root); // 建立重鏈的線段樹
for (int i = 1; i < n; i++)
{
if (dep[d[i][0]] > dep[d[i][1]]) // 調整樹上 每條邊兩端頂點序號 保證邊 a->b 有a<b
swap(d[i][0], d[i][1]);
update(1, 1, tot, w[d[i][1]], d[i][2]); // 初始化樹中的每一條邊在重鏈線段樹上的區間初值
}
}
void work()
{
scanf("%s",ch);
for (; ch[0] != 'D'; scanf("%s",ch))
{
scanf("%d%d", &a, &b);
if (ch[0] == 'Q')
printf("%d\n", find(a, b));
else
update(1, 1, tot, w[d[a][1]], b);
}
}
int main()
{
freopen("data.txt","r",stdin);
freopen("data.out","w",stdout);
init();
work();
return 0;
}
線段樹,好久不寫,難調!!!