没事找事写算法(1)---浅谈树链剖分及DFS序

复习

求LCA

LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。通常我们会用倍增的方式来进行求解。

树上差分

可以修改一个点到树根的距离,并用离线的方式求出树上任两点间的距离。

引入

树上的操作有很多,仅会差分和求LCA是远远不够的。今天我来讲讲用于处理链和子树的树上操作。

正题

DFS序

我们该如何维护子树上的操作?比如给整个子树的每条边加上一个量,再求出子树中每条边的边权和。

这些操作看起来是不是和线段树支持的操作很像呢?如果我们能将树上的每个节点对应线段树上的点,那么我们是不是就可以将树上的操作变为线段树上的操作。

现在我们要解决的问题就是如何将子树上的节点映射在一段区间里。DFS序就可以做到。

DFS序,顾名思义,就是将树的每一个节点都按DFS的顺序编号。

这里写图片描述
如图,这样一颗树,不难发现他的子树节点的DFS序都是连续的。也就是我们可以用数据结构维护这个子树上的信息。注意每个节点的DFS序和节点的编号不一定一样。

DFS序代码如下:

void dfs ( int u , int fa ) {
    in[u] = ++cnt , dfn[cnt] = u ;
    for ( int i = head[u] ; i ; i = edge[i].nxt ) {
        int v = edge[i].to ;
        if ( v != fa ) dfs ( v , u ) ;
    }
    out[u] = cnt ;
}

例子

BZOJ4034
 [HAOI2015]树上操作
有一棵点数为 N 的树,以点 1 为根,且树点有边权。然后有 M 个
操作,分为三种:
操作 1 :把某个节点 x 的点权增加 a 。
操作 2 :把某个节点 x 为根的子树中所有点的点权都增加 a 。
操作 3 :询问某个节点 x 到根的路径中所有点的点权和。
Input
第一行包含两个整数 N, M 。表示点数和操作数。接下来一行 N 个整数,表示树中节点的初始权值。接下来 N-1 
行每行三个正整数 fr, to , 表示该树中存在一条边 (fr, to) 。再接下来 M 行,每行分别表示一次操作。其中
第一个数表示该操作的种类( 1-3 ) ,之后接这个操作的参数( x 或者 x a ) 。
Output
对于每个询问操作,输出该询问的答案。答案之间用换行隔开。
Sample Input
5 5
1 2 3 4 5
1 2
1 4
2 3
2 5
3 3
1 2 1
3 5
2 1 2
3 3
Sample Output
6
9
13
HINT
对于 100% 的数据, N,M<=100000 ,且所有输入数据的绝对值都不会超过 10^6 。

DFS序虽然很好理解,但真正做例题时还是有很多细节要考虑(特别是建线段树的部分)。我30分钟码完代码,又调试了将近30分钟才过。
AC代码如下:

#include <iostream>
#include <cstdio>
#include <cstring> 
typedef long long LL ;
const int MAXN = 200000 ;
const int CH_TOP = 2e7 ;
using namespace std ;

char ch[CH_TOP] , *now_w = ch - 1 , *now_r = ch - 1 ;

inline int read ( ) {
    int w = 1 ; 
    while( *++now_r < '0' || *now_r > '9' ) if ( *now_r == '-' ) w = -1 ;
    register int x = *now_r ^ 48 ;
    while( *++now_r >= '0' && *now_r <= '9' )   x = ( x << 1 ) + ( x << 3 ) + ( *now_r ^ 48 ) ;
    return x * w ;
}

inline void write ( LL x ) {
    static char st[20] ; static int top ;
    if ( x < 0 ) *++now_w = '-' , x *= -1 ;
    while( st[++top] = x % 10 ^ 48 , x /= 10 ) ;
    while( *++now_w = st[top] , --top );
    *++now_w = '\n' ;
} 

int in[MAXN + 5] , out[MAXN + 5] , seg[MAXN + 5] , val[MAXN + 5] , cnt ;
bool io[MAXN + 5] ;

struct Node {
    int to , nxt ;
} edge[MAXN + 5] ;

class Graph {
    public :
        Graph ( ) { memset ( head , -1 , sizeof ( head ) ) ; fa[1] = -1 ; }
        void add ( int u , int v ) ;
        void dfs ( int u ) ;
    private :
        int head[MAXN + 5] , tot , fa[MAXN + 5] ;
} G ;

void Graph :: add ( int u ,int v ) {
    edge[tot] = ( Node ) { v , head[u] } , head[u] = tot++ ;
}

void Graph :: dfs ( int u ) {
    in[u] = ++cnt , seg[cnt] = val[u] , io[cnt] = 1 ; 
    for ( int i = head[u] ; ~i ; i = edge[i].nxt ) {
        int v = edge[i].to ; 
        if ( !fa[v] && v != fa[u] )
            fa[v] = u , dfs ( v ) ;
    }
    out[u] = ++cnt , seg[cnt] = -val[u] , io[cnt] = 0 ;
}

class SegmentTree {
    struct Tree {
        LL delta , data , f ;
        #define lson p << 1 
        #define rson p << 1 | 1
    };
    public:
        void Push_Up ( int p ) {
            s[p].data = s[lson].data + s[rson].data ;
            s[p].f = s[lson].f + s[rson].f ;
        }
        void Push_Down ( int p , int len ) {
            if ( s[p].delta ) {
                s[lson].delta += s[p].delta , s[rson].delta += s[p].delta ;
                s[lson].data += s[p].delta * s[lson].f , s[rson].data += s[p].delta * s[rson].f ;
                s[p].delta = 0 ;
            }
        }
        void build ( int p , int l , int r ) ;
        void update ( int p , int l , int r , int x , int y , int cnt ) ;
        LL query ( int p , int l , int r , int x ,int y ) ;
    private:
        Tree s[MAXN * 4 + 5] ;
} S ; 

void SegmentTree :: build ( int p , int l , int r ) {
    if ( l == r ) {
        s[p].data = seg[l] , s[p].delta = 0 ;
        if ( io[l] ) s[p].f = 1 ; else s[p].f = -1 ;
        return ;
    }
    int mid = l + r >> 1 ;
    build ( lson , l , mid ) , build ( rson , mid + 1 , r ) ;
    Push_Up ( p ) ;
}

void SegmentTree :: update ( int p , int l , int r , int x , int y , int cnt ) {    
    if ( y < l || x > r )  return ;
    if ( x <= l && y >= r ) {
        s[p].data += cnt * s[p].f , s[p].delta += cnt ; 
        return  ;
    }
    Push_Down ( p , r - l + 1 ) ;
    int mid = l + r >> 1 ; 
    update ( lson , l , mid , x , y , cnt ) , update ( rson , mid + 1 , r , x , y , cnt ) ;
    Push_Up ( p ) ;
}

LL SegmentTree :: query ( int p , int l , int r , int x , int y ) {
    if ( y < l || x > r )  return 0 ;
    if ( x <= l && y >= r ) return s[p].data ;
    Push_Down ( p , r - l + 1 ) ;
    int mid = l + r >> 1 ;
    return query ( lson , l , mid , x , y ) + query ( rson , mid + 1 , r , x , y ) ;    
}

int main ( ) {
    fread ( ch , 1 , CH_TOP , stdin ) ;

    int n = read ( ) , q = read ( ) ;

    for ( int i = 1 ; i <= n ; ++i )
        val[i] = read ( ) ; 

    for ( int i = 1 ; i < n ; ++i ) {
        int u = read ( ) , v = read ( ) ;
        G.add ( u , v ) , G.add ( v , u ) ;
    }

    G.dfs ( 1 ) , S.build ( 1 , 1 , cnt ) ;

    while ( q-- ) {
        int opt = read ( ) ;
        if ( opt == 1 ) {
            int x = read ( ) , a = read ( ) ;
            S.update ( 1 , 1 , cnt , in[x] , in[x] , a ) , S.update ( 1 , 1 , cnt , out[x] , out[x] , a ) ;
        }
        else if ( opt == 2 ) {
            int x = read ( ) , a = read ( ) ;
            S.update ( 1 , 1 , cnt , in[x] , out[x] , a ) ;
        }
        else {
            int x = read ( ) ;
            write ( S.query ( 1 , 1 , cnt , 1 , in[x] ) ) ;
        }
    }

    fwrite ( ch , 1 , now_w - ch , stdout ) ;

    return 0 ;
}

树链剖分

如果说DFS序是用来维护子树的,那么树链剖分就是用来维护树上一条到树根的链。

首先先我们先引入几个概念:
假设当前我们在节点u。
1、重儿子:u的子节点中子树大小最大的儿子。
2、轻儿子:u的其它子节点。
3、重边:点u与其重儿子的连边。
4、轻边:点u与其轻儿子的连边。
5、重链:由重边连成的路径。
6、轻链:由轻边连成的路径。

这里写图片描述

如图,画上红圈的就是这棵树里的重儿子。由节点1,2,4,7,9,组成的就是一条重链。由节点3,6组成的也是一条重链。

找重儿子的代码如下:

void BuildTree ( int u ) {
    depth[u] = depth[fa[u]] + 1 , sz[u] = 1 ;

    for ( int i = head[u] ; i ; i = edge[i].nxt ) {
        int v = edge[i].to ;
        if ( !fa[v] && fa[u] != v ) {
            fa[v] = u , BuildTree ( v ) , sz[u] += sz[v] ;
            if ( sz[son[u]] < sz[v] ) son[u] = v ;
        }
    }
}

建重链的代码如下

void BuildChain ( int u ) {
    if ( u == top[fa[u]] ) top[u] = top[fa[u]] ; else top[u] = u ;

    for ( int i = head[u] ; i ; i = edge[i].nxt ) {
        int v = edge[i].to ;
        if ( fa[v] == u ) BuildChain ( v ) ;
    }
}

例子

求LCA

那么我们可以用树链剖分做什么题呢?既然树链剖分可以维护链上的信息,那么我们就可以用它来求两点间的LCA。
怎么求呢?我们想一下,我们一开始学的最基础的朴素解法是不是把两点一个一个往上爬,直到两点爬到一个相同的点?这个算法在哪里可以优化时间复杂度呢?
对,就是爬得太慢,我们应该用一些高效的方法在树上向上爬。以前我们还学过用倍增求LCA,就是一种快速向上跳的方法。我们在树链剖分里维护了top数组,表示该节点所在重链最上端的节点。我们用top数组作为跳板往上跳也可以快速地求出LCA。

树链剖分求LCA核心代码如下:

inline int query ( int u , int v ) {
    while ( top[u] != top[v] )
        if ( depth[top[u]] > depth[top[v]] ) 
            u = fa[top[u]] ;
        else
            v  = fa[top[v]] ;

    return depth[u] < depth[v] ? u : v ;
}

树链剖分是我认为好的求LCA方式,它求LCA的时间复杂度是O(0.37nlog2n)(鬼知道怎么算的),比倍增快3倍左右。编程复杂度也不比倍增多多少,NOIP中绝对有用。

洛谷P3384树链剖分模板

之前讲的求轻重链方法被卡了,换了一种终于过了(洛谷毒瘤)。一定要记得取模。
AC代码:

#prag\
ma GCC optimize( "O3" )
#include <iostream>
#include <cstdio>
#include <cstring>
const int MAXN = 200000 ;
const int CH_TOP = 2e7;
using namespace std ;

char ch[CH_TOP] , *now_w = ch - 1 , *now_r = ch - 1 ;

inline int read ( ) {
    int w = 1 ; 
    while( *++now_r < '0' || *now_r > '9' ) if ( *now_r == '-' ) w = -1 ;
    register int x = *now_r ^ 48 ;
    while( *++now_r >= '0' && *now_r <= '9' )   x = ( x << 1 ) + ( x << 3 ) + ( *now_r ^ 48 ) ;
    return x * w ;
}

inline void write ( int x ) {
    static char st[20] ; static int top ;
    if ( x < 0 ) *++now_w = '-' , x *= -1 ;
    while( st[++top] = x % 10 ^ 48 , x /= 10 ) ;
    while( *++now_w = st[top] , --top );
    *++now_w = '\n' ;
} 

int val[MAXN + 5] , cnt , MOD , root , w[MAXN + 5] , n , q ;

class SegmentTree {
    struct Tree {
        int delta , data ;
        #define lson p << 1 
        #define rson p << 1 | 1
    };
    public:
        void Push_Up ( int p ) {
            s[p].data = s[lson].data + s[rson].data , s[p].data %= MOD ;
        }
        void Push_Down ( int p , int len ) {
            if ( s[p].delta ) {
                s[lson].delta += s[p].delta , s[rson].delta += s[p].delta ;
                s[lson].data += s[p].delta * ( len - len / 2 ) , s[rson].data += s[p].delta * ( len / 2 ) ;
                s[lson].data %= MOD , s[rson].data %=  MOD ;
                s[p].delta = 0 ;
            }
        }
        void build ( int p , int l , int r ) ;
        void update ( int p , int l , int r , int x , int y , int cnt ) ;
        int query ( int p , int l , int r , int x ,int y ) ;
    private:
        Tree s[MAXN * 4 + 5] ;
} S ; 

void SegmentTree :: build ( int p , int l , int r ) {
    if ( l == r ) {
        s[p].data = w[l] , s[p].delta = 0 ;
        return ;
    }
    int mid = l + r >> 1 ;
    build ( lson , l , mid ) , build ( rson , mid + 1 , r ) ;
    Push_Up ( p ) ;
}

void SegmentTree :: update ( int p , int l , int r , int x , int y , int cnt ) {    
    if ( y < l || x > r )  return ;
    if ( x <= l && y >= r ) {
        s[p].data += cnt * ( r - l + 1 ) , s[p].delta += cnt ; 
        return  ;
    }
    Push_Down ( p , r - l + 1 ) ;
    int mid = l + r >> 1 ; 
    update ( lson , l , mid , x , y , cnt ) , update ( rson , mid + 1 , r , x , y , cnt ) ;
    Push_Up ( p ) ;
}

int SegmentTree :: query ( int p , int l , int r , int x , int y ) {
    if ( y < l || x > r )  return 0 ;
    if ( x <= l && y >= r ) return s[p].data ;
    Push_Down ( p , r - l + 1 ) ;
    int mid = l + r >> 1 ;
    return ( query ( lson , l , mid , x , y ) + query ( rson , mid + 1 , r , x , y ) ) % MOD ;  
}

struct Node {
    int to , nxt ;
} edge[MAXN + 5] ;

class Graph {
    public :
        Graph ( ) { memset ( head , -1 , sizeof ( head ) ) ; fa[root] = 0 ; depth[0] = -1 ; }
        void add ( int u , int v ) ;
        void BuildTree ( int u ) ; void BuildChain ( int u , int Top ) ;
        void UpPath( int u ,int v , int cnt ) ; int QuPath ( int u , int v ) ;
        void UpSubTree ( int x , int cnt ) ; int QuSubTree ( int x ) ; 
    private :
        int head[MAXN + 5] , tot , fa[MAXN + 5] , size[MAXN + 5] , depth[MAXN + 5] , cnt , id[MAXN + 5] , 
           sz[MAXN + 5] , son[MAXN + 5] , top[MAXN + 5] ;
} G ;

void Graph :: add ( int u ,int v ) {
    edge[tot] = ( Node ) { v , head[u] } , head[u] = tot++ ;
}

void Graph :: BuildTree ( int u ) {
    depth[u] = depth[fa[u]] + 1 , sz[u] = 1 ;
    for ( int i = head[u] ; ~i ; i = edge[i].nxt ) {
        int v = edge[i].to ; 
        if ( !fa[v] && v != fa[u] ) {
            fa[v] = u , BuildTree ( v ) , sz[u] += sz[v] ;
            if ( sz[son[u]] < sz[v] ) son[u] = v ; 
        }
    }
}

void Graph :: BuildChain ( int u , int Top ) {
    id[u] = ++cnt , w[cnt] = val[u] ; top[u] = Top ;
    if ( !son[u] ) return ;
    BuildChain ( son[u] , Top ) ;
    for ( int i = head[u] ; ~i ; i = edge[i].nxt ) {
        int v = edge[i].to ;
        if ( v != fa[u] && v != son[u] ) BuildChain ( v , v ) ;
    }
}

void Graph :: UpSubTree ( int x , int cnt ) {
    S.update ( 1 , 1 , n , id[x] , id[x] + sz[x] - 1 , cnt ) ;
}

int Graph :: QuSubTree ( int x ) {
    return S.query ( 1 , 1 , n , id[x] , id[x] + sz[x] - 1 ) % MOD ;
}

void Graph :: UpPath ( int u , int v , int cnt ) {
    while ( top[u] != top[v] )
        if ( depth[top[u]] > depth[top[v]] ) 
            S.update ( 1 , 1 , n , id[top[u]] , id[u] , cnt ) , u = fa[top[u]] ;
        else
            S.update ( 1 , 1 , n , id[top[v]] , id[v] , cnt ) , v = fa[top[v]] ;
    if ( depth[u] < depth[v]) 
        S.update ( 1 , 1 , n , id[u] , id[v] , cnt ) ;
    else
        S.update ( 1 , 1 , n , id[v] , id[u] , cnt ) ;
}

int Graph :: QuPath ( int u , int v ) {
    int ret = 0 ; 
    while ( top[u] != top[v] ) {
        if ( depth[top[u]] > depth[top[v]] ) 
            ret += S.query ( 1 , 1 , n , id[top[u]] , id[u] ) , u = fa[top[u]] ;
        else
            ret += S.query ( 1 , 1 , n , id[top[v]] , id[v] ) , v = fa[top[v]] ;
        ret %= MOD ;
    }
    if ( depth[u] < depth[v]) 
        ret += S.query ( 1 , 1 , n , id[u] , id[v] ) ;
    else
        ret += S.query ( 1 , 1 , n , id[v] , id[u] ) ;
    return ret % MOD ;
}

int main ( ) {
    fread ( ch , 1 , CH_TOP , stdin ) ;

    n = read ( ) , q = read ( ) , root = read ( ) , MOD = read ( ) ;

    for ( int i = 1 ; i <= n ; ++i )
        val[i] = read ( ) ; 

    for ( int i = 1 ; i < n ; ++i ) {
        int u = read ( ) , v = read ( ) ;
        G.add ( u , v ) , G.add ( v , u ) ;
    }

    G.BuildTree ( root ) , G.BuildChain ( root , root ) , S.build ( 1 , 1 , n ) ;

    while ( q-- ) {
        int opt = read ( ) ;
        if ( opt == 1 ) {
            int u = read ( ) , v = read ( ) , w = read ( ) % MOD ;
            G.UpPath ( u , v , w ) ;
        }
        else if ( opt == 2 ) {
            int u = read ( ) , v = read ( ) ;
            write ( G.QuPath ( u , v ) );
        }
        else if ( opt == 3 ) {
            int x = read ( ) , cnt = read ( ) ;
            G.UpSubTree ( x , cnt ) ;
        }
        else {
            int x = read ( ) ;
            write ( G.QuSubTree ( x ) ) ; 
        }
    }

    fwrite ( ch , 1 , now_w - ch , stdout ) ;

    return 0 ;
}

总结

其实DFS序和树链剖分就是一种HASH方式。优点是能快速维护许多树上操作,缺点则是代码量比较大,容易写挂。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值