1. 程式人生 > >樹鏈剖分 — 輕重邊路徑剖分

樹鏈剖分 — 輕重邊路徑剖分

    “在一棵樹上進行路徑的修改、求極值、求和”乍一看只要線段樹就能輕鬆解決,實際上,僅憑線段樹是不能搞定它的。我們需要用到一種貌似高階的複雜演算法——樹鏈剖分。
樹鏈,就是樹上的路徑。剖分,就是把路徑分類為重鏈輕鏈
     記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。

重鏈幾個明顯的性質就是互不重合且所有重鏈覆蓋所有點重鏈之間由一條不在重鏈上的邊(我們稱作輕邊)連線

,然後對於每一條重鏈來說,我們定義他的深度,頂標為根節點的重鏈的深度為1,頂標的父親在深度為x的重鏈上,那麼該重鏈深度為x+1,如圖鏈1-3-4-9-10的深度為1,鏈2-8,鏈5-7的深度為2,鏈6的深度為3。 

那麼我們首先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;
}
線段樹,好久不寫,難調!!!